diff --git a/.eslintrc.json b/.eslintrc.json index c606b8f933b..3fd6dec3d7e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,6 +56,7 @@ "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], "@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }], + "@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }], "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "no-console": "error", "import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package. @@ -141,8 +142,7 @@ } ] } - ], - "no-restricted-imports": ["error", { "patterns": ["src/**/*"] }] + ] } }, { @@ -164,147 +164,6 @@ "tailwindcss/no-contradicting-classname": "error" } }, - { - "files": ["libs/admin-console/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/admin-console/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/angular/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/angular/*", "src/**/*"] }] - } - }, - { - "files": ["libs/auth/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/auth/*", "src/**/*"] }] - } - }, - { - "files": ["libs/billing/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/billing/*", "src/**/*"] }] - } - }, - { - "files": ["libs/common/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/common/*", "src/**/*"] }] - } - }, - { - "files": ["libs/components/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/components/*", "src/**/*", "@bitwarden/angular/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/components/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-components/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/core/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-core/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/history/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-history/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/legacy/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-legacy/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/navigation/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-navigation/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/export/vault-export/vault-export-core/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/vault-export-core/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/export/vault-export/vault-export-ui/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/vault-export-ui/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/importer/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/importer/*", "src/**/*"] }] - } - }, - { - "files": ["libs/node/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/node/*", "src/**/*"] }] - } - }, - { - "files": ["libs/platform/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/platform/*", "src/**/*"] }] - } - }, - { - "files": ["libs/tools/send/send-ui/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/send-ui/*", "src/**/*"] }] - } - }, - { - "files": ["libs/tools/card/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/tools-card/*", "src/**/*"] }] - } - }, - { - "files": ["libs/vault/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/vault/*", "src/**/*"] }] - } - }, { "files": ["apps/browser/src/**/*.ts", "libs/**/*.ts"], "excludedFiles": [ @@ -344,10 +203,30 @@ ] } }, + { + "files": ["**/src/**/*.ts"], + "excludedFiles": ["**/platform/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "**/platform/**/internal", // General internal pattern + // All features that have been converted to barrel files + "**/platform/messaging/**", + "**/src/**/*" // Prevent relative imports across libs. + ] + } + ] + } + }, { "files": ["bitwarden_license/bit-common/src/**/*.ts"], "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/bit-common/*", "src/**/*"] }] + "no-restricted-imports": [ + "error", + { "patterns": ["@bitwarden/bit-common/*", "**/src/**/*"] } + ] } }, { @@ -357,7 +236,12 @@ "no-restricted-imports": [ "error", { - "patterns": ["biwarden_license/**", "@bitwarden/bit-common/*", "@bitwarden/bit-web/*"] + "patterns": [ + "biwarden_license/**", + "@bitwarden/bit-common/*", + "@bitwarden/bit-web/*", + "**/src/**/*" + ] } ], // Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e38277877bb..d7cca18960a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -111,6 +111,7 @@ apps/desktop/desktop_native/core/src/ssh_agent @bitwarden/team-autofill-dev @bit ## Component Library ## .storybook @bitwarden/team-design-system libs/components @bitwarden/team-design-system +libs/ui @bitwarden/team-design-system apps/browser/src/platform/popup/layout @bitwarden/team-design-system apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-design-system apps/web/src/app/layouts @bitwarden/team-design-system @@ -126,6 +127,7 @@ apps/web/src/app/key-management @bitwarden/team-key-management-dev apps/browser/src/key-management @bitwarden/team-key-management-dev apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev +libs/common/src/key-management @bitwarden/team-key-management-dev apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev diff --git a/.github/renovate.json b/.github/renovate.json index 7f3e7464fe3..b5c43cc1d39 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -4,13 +4,9 @@ "enabledManagers": ["cargo", "github-actions", "npm"], "packageRules": [ { - "groupName": "gh minor", + "groupName": "github-action minor", "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor", "patch"] - }, - { - "matchManagers": ["github-actions"], - "commitMessagePrefix": "[deps] BRE:" + "matchUpdateTypes": ["minor"] }, { "matchManagers": ["cargo"], @@ -75,15 +71,41 @@ }, { "matchPackageNames": [ + "@angular-eslint/eslint-plugin-template", + "@angular-eslint/eslint-plugin", + "@angular-eslint/schematics", + "@angular-eslint/template-parser", + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "eslint-config-prettier", + "eslint-import-resolver-typescript", + "eslint-plugin-import", + "eslint-plugin-rxjs-angular", + "eslint-plugin-rxjs", + "eslint-plugin-storybook", + "eslint-plugin-tailwindcss", + "eslint", + "husky", + "lint-staged" + ], + "description": "Architecture owned dependencies", + "commitMessagePrefix": "[deps] Architecture:", + "reviewers": ["team:dept-architecture"] + }, + { + "matchPackageNames": [ + "@emotion/css", "@webcomponents/custom-elements", "concurrently", "cross-env", "del", + "lit", "nord", "patch-package", "prettier", "prettier-plugin-tailwindcss", "rimraf", + "@storybook/web-components-webpack5", "tabbable", "tldts", "wait-on" @@ -102,6 +124,8 @@ "matchPackageNames": [ "@babel/core", "@babel/preset-env", + "@bitwarden/sdk-internal", + "@electron/fuses", "@electron/notarize", "@electron/rebuild", "@ngtools/webpack", @@ -113,7 +137,7 @@ "@types/node", "@types/node-forge", "@types/node-ipc", - "@yao-pkg", + "@yao-pkg/pkg", "babel-loader", "browserslist", "copy-webpack-plugin", @@ -133,6 +157,7 @@ "tsconfig-paths-webpack-plugin", "type-fest", "typescript", + "typescript-strict-plugin", "webpack", "webpack-cli", "webpack-dev-server", @@ -149,12 +174,13 @@ "@angular/cdk", "@angular/cli", "@angular/common", - "@angular/compiler", "@angular/compiler-cli", + "@angular/compiler", "@angular/core", "@angular/forms", + "@angular/platform-browser-dynamic", + "@angular/platform-browser", "@angular/platform", - "@angular/compiler", "@angular/router", "@compodoc/compodoc", "@ng-select/ng-select", @@ -162,8 +188,11 @@ "@storybook/addon-actions", "@storybook/addon-designs", "@storybook/addon-essentials", + "@storybook/addon-interactions", "@storybook/addon-links", "@storybook/angular", + "@storybook/manager-api", + "@storybook/theming", "@types/react", "autoprefixer", "bootstrap", @@ -184,25 +213,11 @@ }, { "matchPackageNames": [ - "@angular-eslint/eslint-plugin", - "@angular-eslint/eslint-plugin-template", - "@angular-eslint/template-parser", "@types/jest", - "@typescript-eslint/eslint-plugin", - "@typescript-eslint/parser", - "eslint", - "eslint-config-prettier", - "eslint-import-resolver-typescript", - "eslint-plugin-import", - "eslint-plugin-rxjs", - "eslint-plugin-rxjs-angular", - "eslint-plugin-storybook", - "eslint-plugin-tailwindcss", - "husky", "jest-junit", "jest-mock-extended", "jest-preset-angular", - "lint-staged", + "jest-diff", "ts-jest" ], "description": "Secrets Manager owned dependencies", diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index 73d323851e5..653f6591c7f 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -3,26 +3,12 @@ ./apps/browser/src/safari/desktop/Assets.xcassets/AppIcon.appiconset ./apps/browser/src/safari/desktop/Base.lproj ./apps/browser/store/windows/Assets -./bitwarden_license/README.md ./libs/angular/src/directives/cipherListVirtualScroll.directive.ts -./libs/admin-console/README.md -./libs/auth/README.md -./libs/billing/README.md -./libs/common/src/tools/integration/README.md -./libs/platform/README.md -./libs/key-management/README.md -./libs/tools/README.md -./libs/tools/export/vault-export/README.md -./libs/tools/send/README.md -./libs/tools/card/README.md -./libs/vault/README.md -./README.md ./LICENSE_BITWARDEN.txt ./CONTRIBUTING.md ./LICENSE_GPL.txt ./LICENSE.txt ./apps/web/Dockerfile -./apps/web/README.md ./apps/desktop/resources/installerSidebar.bmp ./apps/desktop/resources/appx/SplashScreen.png ./apps/desktop/resources/appx/BadgeLogo.png @@ -30,10 +16,7 @@ ./apps/desktop/resources/appx/StoreLogo.png ./apps/desktop/resources/appx/Wide310x150Logo.png ./apps/desktop/resources/appx/Square44x44Logo.png -./apps/desktop/README.md ./apps/cli/stores/chocolatey/tools/VERIFICATION.txt -./apps/cli/README.md -./apps/browser/README.md ./apps/browser/store/windows/AppxManifest.xml ./apps/browser/src/background/nativeMessaging.background.ts ./apps/browser/src/models/browserComponentState.ts diff --git a/.github/workflows/brew-bump-desktop.yml b/.github/workflows/brew-bump-desktop.yml deleted file mode 100644 index 1b3c99128bf..00000000000 --- a/.github/workflows/brew-bump-desktop.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Bump Desktop Cask - -on: - push: - tags: - - desktop-v** - workflow_dispatch: - -defaults: - run: - shell: bash - -jobs: - update-desktop-cask: - name: Update Bitwarden Desktop Cask - runs-on: macos-13 - steps: - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "brew-bump-workflow-pat" - - - name: Update Homebrew cask - uses: macauley/action-homebrew-bump-cask@445c42390d790569d938f9068d01af39ca030feb # v1.0.0 - with: - # Required, custom GitHub access token with the 'public_repo' and 'workflow' scopes - token: ${{ steps.retrieve-secrets.outputs.brew-bump-workflow-pat }} - org: bitwarden - tap: Homebrew/homebrew-cask - cask: bitwarden - tag: ${{ github.ref }} - revision: ${{ github.sha }} - force: true - dryrun: true diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 7740e418e7b..c9e9f588c83 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -114,8 +114,8 @@ jobs: fi - build: - name: Build + build-source: + name: Build browser source runs-on: ubuntu-22.04 needs: - setup @@ -127,7 +127,7 @@ jobs: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -169,21 +169,87 @@ jobs: zip -r browser-source.zip browser-source - name: Upload browser source - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip if-no-files-found: error + + build: + name: Build + runs-on: ubuntu-22.04 + needs: + - setup + - locales-test + - build-source + env: + _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} + _NODE_VERSION: ${{ needs.setup.outputs.node_version }} + strategy: + matrix: + include: + - name: "chrome" + npm_command: "dist:chrome" + archive_name: "dist-chrome.zip" + artifact_name: "dist-chrome-MV3" + - name: "edge" + npm_command: "dist:edge" + archive_name: "dist-edge.zip" + artifact_name: "dist-edge-MV3" + - name: "firefox" + npm_command: "dist:firefox" + archive_name: "dist-firefox.zip" + artifact_name: "dist-firefox" + - name: "firefox-mv3" + npm_command: "dist:firefox:mv3" + archive_name: "dist-firefox.zip" + artifact_name: "DO-NOT-USE-FOR-PROD-dist-firefox-MV3" + - name: "opera" + npm_command: "dist:opera" + archive_name: "dist-opera.zip" + artifact_name: "dist-opera" + - name: "opera-mv3" + npm_command: "dist:opera:mv3" + archive_name: "dist-opera.zip" + artifact_name: "DO-NOT-USE-FOR-PROD-dist-opera-MV3" + steps: + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + cache: 'npm' + cache-dependency-path: '**/package-lock.json' + node-version: ${{ env._NODE_VERSION }} + + - name: Print environment + run: | + node --version + npm --version + + - name: Download browser source + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: browser-source-${{ env._BUILD_NUMBER }}.zip + + - name: Unzip browser source artifact + run: | + unzip browser-source.zip + rm browser-source.zip + - name: NPM setup run: npm ci working-directory: browser-source/ - - name: Download SDK Artifacts + - name: Download SDK artifacts if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: - github_token: ${{secrets.GITHUB_TOKEN}} + github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-wasm-internal.yml workflow_conclusion: success branch: ${{ inputs.sdk_branch }} @@ -195,85 +261,19 @@ jobs: - name: Override SDK if: ${{ inputs.sdk_branch != '' }} working-directory: browser-source/ - run: | - npm link ../sdk-internal + run: npm link ../sdk-internal - - name: Build Chrome - run: npm run dist:chrome + - name: Build extension + run: npm run ${{ matrix.npm_command }} working-directory: browser-source/apps/browser - - name: Upload Chrome MV3 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + - name: Upload extension artifact + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/dist/dist-chrome.zip + name: ${{ matrix.artifact_name }}-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/${{ matrix.archive_name }} if-no-files-found: error - - name: Build Edge - run: npm run dist:edge - working-directory: browser-source/apps/browser - - - name: Upload Edge artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: dist-edge-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/dist/dist-edge.zip - if-no-files-found: error - - - name: Build Edge (MV3) - run: npm run dist:edge:mv3 - working-directory: browser-source/apps/browser - - - name: Upload Edge MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/dist/dist-edge.zip - if-no-files-found: error - - - name: Build Firefox - run: npm run dist:firefox - working-directory: browser-source/apps/browser - - - name: Upload Firefox artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: dist-firefox-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/dist/dist-firefox.zip - if-no-files-found: error - - - name: Build Firefox (MV3) - run: npm run dist:firefox:mv3 - working-directory: browser-source/apps/browser - - - name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/dist/dist-firefox.zip - if-no-files-found: error - - - name: Build Opera - run: npm run dist:opera - working-directory: browser-source/apps/browser - - - name: Upload Opera artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: dist-opera-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/dist/dist-opera.zip - if-no-files-found: error - - - name: Build Opera (MV3) - run: npm run dist:opera:mv3 - working-directory: browser-source/apps/browser - - - name: Upload Opera MV3 artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 - with: - name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip - path: browser-source/apps/browser/dist/dist-opera.zip - if-no-files-found: error build-safari: name: Build Safari @@ -405,7 +405,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/dist-safari.zip @@ -448,6 +448,7 @@ jobs: upload_sources: true upload_translations: false + check-failures: name: Check for failures if: always() @@ -455,6 +456,7 @@ jobs: needs: - setup - locales-test + - build-source - build - build-safari - crowdin-push diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index d480879fb15..d5d78811cda 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -163,14 +163,14 @@ jobs: matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt - name: Upload unix zip asset - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload unix checksum asset - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -324,14 +324,14 @@ jobs: -t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt - name: Upload windows zip asset - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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 if-no-files-found: error - name: Upload windows checksum asset - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -339,7 +339,7 @@ jobs: - name: Upload Chocolatey asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg @@ -350,7 +350,7 @@ jobs: - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip @@ -421,14 +421,14 @@ jobs: run: sudo snap remove bw - name: Upload snap asset - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload snap checksum asset - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 2a59dc28fc9..ca681dac6b0 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -207,7 +207,7 @@ jobs: npm link ../sdk-internal - name: Cache Native Module - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: | @@ -232,42 +232,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + 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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -280,7 +280,7 @@ jobs: sudo npm run pack:lin:flatpak - name: Upload flatpak artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: com.bitwarden.desktop.flatpak path: apps/desktop/dist/com.bitwarden.desktop.flatpak @@ -373,7 +373,7 @@ jobs: npm link ../sdk-internal - name: Cache Native Module - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: | @@ -428,91 +428,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: 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 - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -561,14 +561,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -681,7 +681,7 @@ jobs: npm link ../sdk-internal - name: Cache Native Module - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: | @@ -749,14 +749,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -869,7 +869,7 @@ jobs: npm link ../sdk-internal - name: Cache Native Module - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: | @@ -918,28 +918,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -990,14 +990,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -1117,7 +1117,7 @@ jobs: npm link ../sdk-internal - name: Cache Native Module - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: | @@ -1166,7 +1166,7 @@ jobs: run: npm run pack:mac:mas - name: Upload .pkg artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -1193,9 +1193,11 @@ jobs: if: | github.event_name != 'pull_request_target' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop') - uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0 + uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0 with: channel-id: C074F5UESQ0 + method: chat.postMessage + token: ${{ steps.retrieve-slack-secret.outputs.slack-bot-token }} payload: | { "blocks": [ @@ -1209,7 +1211,6 @@ jobs: ] } env: - SLACK_BOT_TOKEN: ${{ steps.retrieve-slack-secret.outputs.slack-bot-token }} BUILD_NUMBER: ${{ needs.setup.outputs.build_number }} @@ -1252,14 +1253,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -1372,7 +1373,7 @@ jobs: npm link ../sdk-internal - name: Cache Native Module - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 id: cache with: path: | @@ -1424,7 +1425,7 @@ jobs: zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index c686b46d51a..b7e8a51897c 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -164,7 +164,7 @@ jobs: run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build - name: Upload ${{ matrix.name }} artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: web-${{ env._VERSION }}-${{ matrix.name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip @@ -274,7 +274,7 @@ jobs: - name: Build Docker image id: build-docker - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 + uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: context: apps/web file: apps/web/Dockerfile @@ -303,14 +303,14 @@ jobs: - name: Scan Docker image id: container-scan - uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0 + uses: anchore/scan-action@869c549e657a088dc0441b08ce4fc0ecdac2bb65 # v5.3.0 with: image: ${{ steps.image-name.outputs.name }} fail-build: false output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 0efd9d22f17..75a07431942 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -43,7 +43,7 @@ jobs: - name: Cache NPM id: npm-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: "~/.npm" key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }} @@ -56,7 +56,7 @@ jobs: run: npm run build-storybook:ci - name: Publish to Chromatic - uses: chromaui/action@dd2eecb9bef44f54774581f4163b0327fd8cf607 # v11.16.3 + uses: chromaui/action@8a12962215a66cd05b1ac5b0f1c08768d1aab155 # v11.25.0 with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index f99cecf91d6..027a2f11e55 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -21,8 +21,17 @@ jobs: - app_name: web crowdin_project_id: "308189" steps: + - name: Generate GH App token + uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 + id: app-token + with: + app-id: ${{ secrets.BW_GHAPP_ID }} + private-key: ${{ secrets.BW_GHAPP_KEY }} + - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + token: ${{ steps.app-token.outputs.token }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -35,13 +44,6 @@ jobs: with: keyvault: "bitwarden-ci" secrets: "crowdin-api-token, github-gpg-private-key, github-gpg-private-key-passphrase" - - - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 - id: app-token - with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} - name: Download translations uses: bitwarden/gh-actions/crowdin@main diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index b5e84ff875b..9b890491282 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -63,14 +63,14 @@ jobs: runs-on: ubuntu-22.04 outputs: environment: ${{ steps.config.outputs.environment }} - environment-url: ${{ steps.config.outputs.environment-url }} - environment-name: ${{ steps.config.outputs.environment-name }} - environment-artifact: ${{ steps.config.outputs.environment-artifact }} - azure-login-creds: ${{ steps.config.outputs.azure-login-creds }} - retrieve-secrets-keyvault: ${{ steps.config.outputs.retrieve-secrets-keyvault }} - sync-utility: ${{ steps.config.outputs.sync-utility }} - sync-delete-destination-files: ${{ steps.config.outputs.sync-delete-destination-files }} - slack-channel-name: ${{ steps.config.outputs.slack-channel-name }} + environment_url: ${{ steps.config.outputs.environment_url }} + environment_name: ${{ steps.config.outputs.environment_name }} + environment_artifact: ${{ steps.config.outputs.environment_artifact }} + azure_login_creds: ${{ steps.config.outputs.azure_login_creds }} + retrive_secrets_keyvault: ${{ steps.config.outputs.retrive_secrets_keyvault }} + sync_utility: ${{ steps.config.outputs.sync_utility }} + sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }} + slack_channel_name: ${{ steps.config.outputs.slack_channel_name }} steps: - name: Configure id: config @@ -81,48 +81,48 @@ jobs: case ${{ inputs.environment }} in "USQA") - echo "azure-login-creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "EUQA") - echo "azure-login-creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "USPROD") - echo "azure-login-creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "EUPROD") - echo "azure-login-creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "USDEV") - echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-dev" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-dev" >> $GITHUB_OUTPUT ;; esac # Set the sync utility to use for deployment to the environment (az-sync or azcopy) - echo "sync-utility=azcopy" >> $GITHUB_OUTPUT + echo "sync_utility=azcopy" >> $GITHUB_OUTPUT - name: Environment Protection env: @@ -168,10 +168,10 @@ jobs: fi approval: - name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }} + name: Approval for Deployment to ${{ needs.setup.outputs.environment_name }} needs: setup runs-on: ubuntu-22.04 - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} steps: - name: Success Code run: exit 0 @@ -181,9 +181,9 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} outputs: - artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }} + artifact_build_commit: ${{ steps.set-artifact-commit.outputs.commit }} steps: - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' if: ${{ inputs.build-web-run-id }} @@ -242,7 +242,7 @@ jobs: run: | # If run-id was used, get the commit from the download-latest-artifacts-run-id step if [ "${{ inputs.build-web-run-id }}" ]; then - echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. @@ -251,7 +251,7 @@ jobs: else # Set the commit to the output of step download-latest-artifacts. - echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT fi notify-start: @@ -271,11 +271,11 @@ jobs: id: slack-message with: project: Clients - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} tag: ${{ inputs.branch-or-tag }} - slack-channel: ${{ needs.setup.outputs.slack-channel-name }} + slack-channel: ${{ needs.setup.outputs.slack_channel_name }} event: 'start' - commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} + commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -287,7 +287,7 @@ jobs: - name: Display commit SHA run: | REPO_URL="https://github.com/bitwarden/clients/commit" - COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}" + COMMIT_SHA="${{ needs.artifact-check.outputs.artifact_build_commit }}" echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY azure-deploy: @@ -299,9 +299,9 @@ jobs: runs-on: ubuntu-22.04 env: _ENVIRONMENT: ${{ needs.setup.outputs.environment }} - _ENVIRONMENT_URL: ${{ needs.setup.outputs.environment-url }} - _ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment-name }} - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} + _ENVIRONMENT_URL: ${{ needs.setup.outputs.environment_url }} + _ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment_name }} + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} steps: - name: Create GitHub deployment uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 @@ -309,31 +309,31 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' - ref: ${{ needs.artifact-check.outputs.artifact-build-commit }} + ref: ${{ needs.artifact-check.outputs.artifact_build_commit }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: - creds: ${{ secrets[needs.setup.outputs.azure-login-creds] }} + creds: ${{ secrets[needs.setup.outputs.azure_login_creds] }} - name: Retrieve Storage Account connection string for az sync - if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }} + if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} id: retrieve-secrets-az-sync uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} + keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-dev-key-temp" - name: Retrieve Storage Account name and SPN credentials for azcopy - if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }} + if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} id: retrieve-secrets-azcopy uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} + keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant" - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' @@ -363,7 +363,7 @@ jobs: run: unzip ${{ env._ENVIRONMENT_ARTIFACT }} - name: Sync to Azure Storage Account using az storage blob sync - if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }} + if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} working-directory: apps/web run: | az storage blob sync \ @@ -373,7 +373,7 @@ jobs: --delete-destination=${{ inputs.force-delete-destination }} - name: Sync to Azure Storage Account using azcopy - if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }} + if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} working-directory: apps/web env: AZCOPY_AUTO_LOGIN_TYPE: SPN @@ -397,7 +397,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} state: 'success' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -406,7 +406,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} state: 'failure' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -424,11 +424,11 @@ jobs: uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: project: Clients - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} tag: ${{ inputs.branch-or-tag }} slack-channel: ${{ needs.notify-start.outputs.channel_id }} event: ${{ needs.azure-deploy.result }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} - commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} + commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} update-ts: ${{ needs.notify-start.outputs.ts }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a907618bd36..5bc566202c6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -34,6 +34,7 @@ jobs: ! -path "*/.DS_Store" \ ! -path "*/*locales/*" \ ! -path "./.github/*" \ + ! -path "*/README.md" \ ! -path "*/Cargo.toml" \ ! -path "*/Cargo.lock" \ ! -path "./apps/desktop/macos/*" \ @@ -54,21 +55,25 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ steps.retrieve-node-version.outputs.node_version }} + - name: Install Node dependencies + run: npm ci + + - name: Lint unowned dependencies + run: npm run lint:dep-ownership + - name: Run linter - run: | - npm ci - npm run lint + run: npm run lint rust: name: Run Rust lint on ${{ matrix.os }} - runs-on: ${{ matrix.os || 'ubuntu-latest' }} + runs-on: ${{ matrix.os || 'ubuntu-24.04' }} strategy: matrix: os: - - ubuntu-latest - - macos-latest - - windows-latest + - ubuntu-24.04 + - macos-14 + - windows-2022 steps: - name: Checkout repo diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 0a561306797..ff85a30d3f6 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -43,8 +43,8 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version-output.outputs.version }} - deployment-id: ${{ steps.deployment.outputs.deployment_id }} + release_version: ${{ steps.version-output.outputs.version }} + deployment_id: ${{ steps.deployment.outputs.deployment_id }} defaults: run: working-directory: . @@ -88,7 +88,7 @@ jobs: needs: setup if: inputs.snap_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -125,7 +125,7 @@ jobs: needs: setup if: inputs.choco_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -165,7 +165,7 @@ jobs: needs: setup if: inputs.npm_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -222,7 +222,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -230,4 +230,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 5ef378ad439..af24083e973 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -7,19 +7,18 @@ on: publish_type: description: 'Publish Options' required: true - default: 'Initial Publish' + default: 'Publish' type: choice options: - - Initial Publish - - Republish + - Publish - Dry Run version: description: 'Version to publish (default: latest desktop release)' required: true type: string default: latest - rollout_percentage: - description: 'Staged Rollout Percentage' + electron_rollout_percentage: + description: 'Staged Rollout Percentage for Electron' required: true default: '10' type: string @@ -39,10 +38,10 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} - tag-name: ${{ steps.version.outputs.tag_name }} - deployment-id: ${{ steps.deployment.outputs.deployment_id }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} + tag_name: ${{ steps.version.outputs.tag_name }} + deployment_id: ${{ steps.deployment.outputs.deployment_id }} steps: - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -76,7 +75,7 @@ jobs: fi - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -100,7 +99,7 @@ jobs: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' environment: 'Desktop - Production' - description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release-channel.outputs.channel }} from branch ${{ github.ref_name }}' + description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release_channel.outputs.channel }} from branch ${{ github.ref_name }}' task: release electron-blob: @@ -108,8 +107,8 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -136,8 +135,8 @@ jobs: - name: Set staged rollout percentage env: - RELEASE_CHANNEL: ${{ needs.setup.outputs.release-channel }} - ROLLOUT_PCT: ${{ inputs.rollout_percentage }} + RELEASE_CHANNEL: ${{ needs.setup.outputs.release_channel }} + ROLLOUT_PCT: ${{ inputs.electron_rollout_percentage }} run: | echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-linux.yml @@ -163,7 +162,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -171,7 +170,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} snap: name: Deploy Snap @@ -179,8 +178,8 @@ jobs: needs: setup if: inputs.snap_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -223,8 +222,8 @@ jobs: needs: setup if: inputs.choco_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -284,7 +283,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -292,4 +291,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 75442187516..3e54e79a303 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -23,7 +23,7 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} + release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: ${{ github.event.inputs.release_type }} project-type: ts @@ -118,7 +118,7 @@ jobs: - name: Rename build artifacts env: - PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + 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 @@ -128,16 +128,16 @@ jobs: - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 with: - artifacts: 'browser-source-${{ needs.setup.outputs.release-version }}.zip, - dist-chrome-${{ needs.setup.outputs.release-version }}.zip, - dist-opera-${{ needs.setup.outputs.release-version }}.zip, - dist-firefox-${{ needs.setup.outputs.release-version }}.zip, - dist-edge-${{ needs.setup.outputs.release-version }}.zip' + artifacts: 'browser-source-${{ needs.setup.outputs.release_version }}.zip, + dist-chrome-${{ needs.setup.outputs.release_version }}.zip, + dist-opera-${{ needs.setup.outputs.release_version }}.zip, + dist-firefox-${{ needs.setup.outputs.release_version }}.zip, + dist-edge-${{ needs.setup.outputs.release_version }}.zip' commit: ${{ github.sha }} - tag: "browser-v${{ needs.setup.outputs.release-version }}" - name: "Browser v${{ needs.setup.outputs.release-version }}" + tag: "browser-v${{ needs.setup.outputs.release_version }}" + name: "Browser v${{ needs.setup.outputs.release_version }}" body: "" token: ${{ secrets.GITHUB_TOKEN }} draft: true diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 05c53f9752d..a1bc36cf85e 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -23,7 +23,7 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} + release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts @@ -73,9 +73,9 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 env: - PKG_VERSION: ${{ needs.setup.outputs.release-version }} + PKG_VERSION: ${{ needs.setup.outputs.release_version }} with: artifacts: "apps/cli/bw-oss-windows-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-windows-sha256-${{ env.PKG_VERSION }}.txt, diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index a940ce289ff..ea0feb10e3d 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -16,9 +16,9 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} - branch-name: ${{ steps.branch.outputs.branch-name }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} + branch_name: ${{ steps.branch.outputs.branch_name }} build_number: ${{ steps.increment-version.outputs.build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: @@ -47,7 +47,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: 'Initial Release' project-type: ts @@ -63,7 +63,7 @@ jobs: echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -102,7 +102,7 @@ jobs: git push -u origin $branch_name - echo "branch-name=$branch_name" >> $GITHUB_OUTPUT + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT - name: Get Node Version id: retrieve-node-version @@ -116,7 +116,7 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -126,7 +126,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -158,45 +158,45 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + 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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: ${{ needs.setup.outputs.release-channel }}-linux.yml - path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml + name: ${{ needs.setup.outputs.release_channel }}-linux.yml + path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml if-no-files-found: error @@ -209,14 +209,14 @@ jobs: shell: pwsh working-directory: apps/desktop env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -299,94 +299,94 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: 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 - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: ${{ needs.setup.outputs.release-channel }}.yml - path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml + name: ${{ needs.setup.outputs.release_channel }}.yml + path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml if-no-files-found: error @@ -395,7 +395,7 @@ jobs: runs-on: macos-13 needs: setup env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -405,7 +405,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -426,14 +426,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -529,7 +529,7 @@ jobs: - setup - macos-build env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -539,7 +539,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -560,14 +560,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -707,31 +707,31 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: - name: ${{ needs.setup.outputs.release-channel }}-mac.yml - path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml + name: ${{ needs.setup.outputs.release_channel }}-mac.yml + path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml if-no-files-found: error @@ -742,7 +742,7 @@ jobs: - setup - macos-build env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -752,7 +752,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -773,14 +773,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -915,7 +915,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Upload .pkg artifact - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -939,7 +939,7 @@ jobs: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' environment: 'Desktop - Beta' - description: 'Deployment ${{ needs.setup.outputs.release-version }} to channel ${{ needs.setup.outputs.release-channel }} from branch ${{ needs.setup.outputs.branch-name }}' + description: 'Deployment ${{ needs.setup.outputs.release_version }} to channel ${{ needs.setup.outputs.release_channel }} from branch ${{ needs.setup.outputs.branch_name }}' task: release - name: Login to Azure @@ -963,7 +963,7 @@ jobs: - name: Rename .pkg to .pkg.archive env: - PKG_VERSION: ${{ needs.setup.outputs.release-version }} + PKG_VERSION: ${{ needs.setup.outputs.release_version }} working-directory: apps/desktop/artifacts run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive @@ -1020,5 +1020,5 @@ jobs: git config --global url."https://".insteadOf ssh:// - name: Remove branch env: - BRANCH: ${{ needs.setup.outputs.branch-name }} + BRANCH: ${{ needs.setup.outputs.branch_name }} run: git push origin --delete $BRANCH diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index d9394347f60..3285ad468d6 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -22,8 +22,8 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts @@ -49,7 +49,7 @@ jobs: monorepo-project: desktop - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -96,11 +96,11 @@ jobs: file_path: "apps/desktop/artifacts/sha256-checksums.txt" - name: Create Release - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 - if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 + if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} env: PKG_VERSION: ${{ steps.version.outputs.version }} - RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} + RELEASE_CHANNEL: ${{ steps.release_channel.outputs.channel }} with: artifacts: "apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-amd64.deb, apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-x86_64.rpm, diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index faa398f6d67..0301b814796 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -81,7 +81,7 @@ jobs: - name: Create release if: ${{ github.event.inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 + uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 with: name: "Web v${{ needs.setup.outputs.release_version }}" commit: ${{ github.sha }} diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 9935ef7674e..ac2733e765b 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -66,7 +66,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 id: app-token with: app-id: ${{ secrets.BW_GHAPP_ID }} @@ -115,7 +115,7 @@ jobs: version: ${{ inputs.version_number_override }} - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 id: app-token with: app-id: ${{ secrets.BW_GHAPP_ID }} @@ -452,7 +452,7 @@ jobs: - setup steps: - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 id: app-token with: app-id: ${{ secrets.BW_GHAPP_ID }} @@ -461,6 +461,7 @@ jobs: - name: Check out main branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: + fetch-depth: 0 ref: main token: ${{ steps.app-token.outputs.token }} @@ -490,6 +491,7 @@ jobs: git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT git push -u origin $destination_branch fi + } # Cherry-pick from 'main' into 'rc' cherry_pick browser main rc diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 6166ac79b1a..ac7f0ae6f71 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -31,7 +31,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36 + uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0 + uses: github/codeql-action/upload-sarif@d68b2d4edb4189fd2a5366ac14e72027bd4b37dd # v3.28.2 with: sarif_file: cx_result.sarif @@ -66,10 +66,9 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with SonarCloud - uses: sonarsource/sonarcloud-github-action@02ef91109b2d589e757aefcfb2854c2783fd7b19 # v4.0.0 + uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: args: > -Dsonar.organization=${{ github.repository_owner }} diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index 91250a443f2..4ec3af3be97 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -1,4 +1,5 @@ name: Staged Rollout Desktop +run-name: Staged Rollout Desktop - ${{ inputs.rollout_percentage }}% on: workflow_dispatch: diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 6caa7b99331..abb292f53f3 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: 'Run stale action' - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 + uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: stale-issue-label: 'needs-reply' stale-pr-label: 'needs-changes' diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index f41261cb39a..ef46dbc867d 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Generate GH App token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 id: app-token with: app-id: ${{ secrets.BW_GHAPP_ID }} diff --git a/angular.json b/angular.json index 7053050262e..665d810cf4e 100644 --- a/angular.json +++ b/angular.json @@ -147,6 +147,10 @@ "./tsconfig.json", "-e", "json", + "--disableInternal", + "--disableLifeCycleHooks", + "--disablePrivate", + "--disableProtected", "-d", ".", "--disableRoutesGraph" @@ -165,6 +169,10 @@ "./tsconfig.json", "-e", "json", + "--disableInternal", + "--disableLifeCycleHooks", + "--disablePrivate", + "--disableProtected", "-d", ".", "--disableRoutesGraph" diff --git a/apps/browser/package.json b/apps/browser/package.json index 7c839ecc3d9..c37e7c24199 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,10 +1,10 @@ { "name": "@bitwarden/browser", - "version": "2024.12.3", + "version": "2025.1.1", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", - "build:edge": "cross-env BROWSER=edge webpack", + "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 webpack", "build:firefox": "cross-env BROWSER=firefox webpack", "build:opera": "cross-env BROWSER=opera webpack", "build:safari": "cross-env BROWSER=safari webpack", @@ -30,6 +30,7 @@ "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari", "test": "jest", "test:watch": "jest --watch", - "test:watch:all": "jest --watchAll" + "test:watch:all": "jest --watchAll", + "test:clearCache": "jest --clear-cache" } } diff --git a/apps/browser/postcss.config.js b/apps/browser/postcss.config.js index c4513687e89..83e237f06e5 100644 --- a/apps/browser/postcss.config.js +++ b/apps/browser/postcss.config.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef */ +/* eslint-disable no-undef, @typescript-eslint/no-require-imports */ module.exports = { plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")], }; diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 7ba554b13b7..625454de8c6 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -84,7 +84,7 @@ "message": "انضم إلى المنظمة" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "انضم إلى $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -93,7 +93,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "أنهي الانضمام إلى هذه المؤسسة عن طريق تعيين كلمة مرور رئيسية." }, "tab": { "message": "علامة تبويب" @@ -120,7 +120,7 @@ "message": "نسخ كلمة المرور" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "انسخ عبارة المرور" }, "copyNote": { "message": "نسخ الملاحظة" @@ -138,31 +138,31 @@ "message": "نسخ رمز الأمان" }, "copyName": { - "message": "Copy name" + "message": "انسخ الاسم" }, "copyCompany": { - "message": "Copy company" + "message": "انسخ الشركة" }, "copySSN": { - "message": "Copy Social Security number" + "message": "انسخ رَقْم الضمان الاجتماعي" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "نسخ رَقْم جواز السفر" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "نسخ رَقْم الترخيص" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "نسخ المفتاح الخاص" }, "copyPublicKey": { - "message": "Copy public key" + "message": "نسخ المفتاح العام" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "نسخ البصمة" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "نسخ $FIELD$", "placeholders": { "field": { "content": "$1", @@ -171,13 +171,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "نسخ الموقع الإلكتروني" }, "copyNotes": { - "message": "Copy notes" + "message": "نسخ الملاحظات" }, "fill": { - "message": "Fill", + "message": "ملء", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "هوية التعبئة التلقائية" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "ملء رمز التحقق" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "ملء رمز التحقق", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -239,16 +239,16 @@ "message": "إضافة عنصر" }, "accountEmail": { - "message": "Account email" + "message": "البريد الإلكتروني للحساب" }, "requestHint": { - "message": "Request hint" + "message": "طلب تلميح" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "طلب تلميح كلمة المرور" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "أدخل عنوان البريد الإلكتروني لحسابك وسيُرسل تلميح كلمة المرور الخاصة بك إليك" }, "passwordHint": { "message": "تلميح كلمة المرور" @@ -281,25 +281,25 @@ "message": "تغيير كلمة المرور الرئيسية" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "هل تريد المتابعة إلى تطبيق الويب؟" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "استكشف المزيد من الميزات لحساب Bitwarden الخاص بك على تطبيق الويب." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "هل تريد المتابعة إلى مركز المساعدة؟" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "تعرف على المزيد حول كيفية استخدام Bitwarden في مركز المساعدة." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "هل تريد المتابعة إلى متجر إضافات المتصفح؟" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "ساعد الآخرين في معرفة ما إذا كان Bitwarden مناسب لهم. قم بزيارة متجر إضافات المتصفح الخاص بك واترك تقييمًا الآن." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "يمكنك تغيير كلمة المرور الرئيسية الخاصة بك على تطبيق ويب الخاص ب Bitwarden." }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", @@ -316,43 +316,43 @@ "message": "تسجيل الخروج" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "حول Bitwarden" }, "about": { "message": "عن التطبيق" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "المزيد من Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "هل تريد المتابعة إلى bitwarden.com؟" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden للأعمال التجارية" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "مصادق Bitwarden" }, "continueToAuthenticatorPageDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" + "message": "مصادق Bitwarden يسمح لك بتخزين مفاتيح المصادقة وإنشاء رموز لمرة واحدة المستندة إلى الوقت لعمليات المصادقة الثنائية. تعرف على المزيد على موقع bitwarden.com" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "مدير الأسرار من Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "قم بتخزين وإدارة ومشاركة أسرار التطوير مع مدير الأسرار من Bitwarden. تعرف على المزيد في موقع bitwarden.com." }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "أنشئ تجارِب تسجيل دخول سلسة وآمنة خالية من كلمات المرور التقليدية مع Passwordless.dev. تعلم المزيد على موقع bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Bitwarden للعائلات المجاني" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "أنت مؤهل للحصول على Bitwarden للعائلات المجاني. أحصل على هذا العرض اليوم عبر تطبيق الويب." }, "version": { "message": "الإصدار" @@ -373,22 +373,22 @@ "message": "تحرير المجلّد" }, "newFolder": { - "message": "New folder" + "message": "مجلد جديد" }, "folderName": { - "message": "Folder name" + "message": "أسم المجلد" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "لا توجد مجلدات مضافة" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "أنشئ مجلدات لتنظيم عناصر المخزن الخاصة بك" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "هل أنت متأكد أنك تريد حذف هذا المجلد نهائيًا؟" }, "deleteFolder": { "message": "حذف المجلّد" @@ -431,7 +431,7 @@ "message": "قم بإنشاء كلمات مرور قوية وفريدة لتسجيلات الدخول الخاصة بك." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "تطبيق ويب Bitwarden" }, "importItems": { "message": "استيراد العناصر" @@ -443,7 +443,7 @@ "message": "توليد كلمة مرور" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "توليد عبارة المرور" }, "regeneratePassword": { "message": "إعادة توليد كلمة المرور" @@ -454,9 +454,6 @@ "length": { "message": "الطول" }, - "passwordMinLength": { - "message": "الحد الأدنى لطول كلمة السر" - }, "uppercase": { "message": "أحرف كبيرة (من A إلى Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -474,11 +471,11 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "تضمين", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "تضمين أحرف ذات نسق كبير", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -486,7 +483,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "تضمين أحرف ذات نسق صغير", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -494,7 +491,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "تضمين أرقام", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -502,7 +499,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "تضمين أحرف خاصة", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -528,16 +525,12 @@ "minSpecial": { "message": "الحد الأدنى من الأحرف الخاصة" }, - "avoidAmbChar": { - "message": "تجنب الأحرف الغامضة", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "تجنب الأحرف المبهمة", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "طُبقت متطلبات سياسة المؤسسة على خيارات المولد الخاصة بك.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -571,19 +564,19 @@ "message": "المفضلات" }, "unfavorite": { - "message": "Unfavorite" + "message": "إزالة من المفضلة" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "تم إضافة العنصر إلى المفضلات" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "تم إزالة العنصر من المفضلات" }, "notes": { "message": "الملاحظات" }, "privateNote": { - "message": "Private note" + "message": "ملاحظة سرية" }, "note": { "message": "الملاحظة" @@ -604,10 +597,10 @@ "message": "بدء" }, "launchWebsite": { - "message": "Launch website" + "message": "تشغيل الموقع" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "تشغيل الموقع $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -628,7 +621,7 @@ "message": "الأخرى" }, "unlockMethods": { - "message": "Unlock options" + "message": "فتح الخيارات" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "أعدنّ طريقة إلغاء القُفْل لتغيير إجراء مهلة المخزن الخاص بك." @@ -637,13 +630,13 @@ "message": "إعداد طريقة إلغاء القفل في الإعدادات" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "مهلة الجَلسة" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "مهلة الخزنة" }, "otherOptions": { - "message": "Other options" + "message": "خيارات أخرى" }, "rateExtension": { "message": "قيِّم هذه الإضافة" @@ -658,13 +651,13 @@ "message": "خزانتك مقفلة. قم بتأكيد هويتك للمتابعة." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "المخزن الخاص بك مقفل" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "حسابك مقفل" }, "or": { - "message": "or" + "message": "أو" }, "unlock": { "message": "إلغاء القفل" @@ -689,7 +682,7 @@ "message": "نفذ وقت الخزانة" }, "vaultTimeout1": { - "message": "Timeout" + "message": "المهلة" }, "lockNow": { "message": "إقفل الآن" @@ -743,16 +736,16 @@ "message": "الأمان" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "تأكيد كلمة المرور الرئيسية" }, "masterPassword": { - "message": "Master password" + "message": "كلمة المرور الرئيسية" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "لا يمكن استعادة كلمة المرور الرئيسية إذا نسيتها!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "تلميح كلمة المرور الرئيسية" }, "errorOccurred": { "message": "لقد حدث خطأ ما" @@ -786,10 +779,10 @@ "message": "تم إنشاء حسابك الجديد! يمكنك الآن تسجيل الدخول." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "تم إنشاء حسابك الجديد!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "لقد قمت بتسجيل الدخول!" }, "youSuccessfullyLoggedIn": { "message": "سجلتَ الدخول بنجاح" @@ -804,7 +797,7 @@ "message": "رمز التحقق مطلوب." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "تم إلغاء المصادقة أو استغرقت وقتا طويلا. الرجاء المحاولة مرة أخرى." }, "invalidVerificationCode": { "message": "رمز التحقق غير صالح" @@ -832,16 +825,16 @@ "message": "مسح رمز QR للمصادقة من صفحة الويب الحالية" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "أجعل التحقق بخطوتين سلس" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden يمكنه تخزين وملء رموز التحقق بخطوتين. أنسخ وألصق المفتاح في هذا الحقل." }, "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." + "message": "Bitwarden يمكنه تخزين وملء رموز التحقق من خطوتين. اختر رمز الكاميرا لأخذ لقطة شاشة لرمز QR المصادق لهذا الموقع، أو أنسخ وألصق المفتاح في هذا الحقل." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "تعرف على المزيد عن المصادقين" }, "copyTOTP": { "message": "نسخ مفتاح المصادقة (TOTP)" @@ -850,28 +843,28 @@ "message": "تم تسجيل الخروج" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "لقد تم تسجيل خروجك من حسابك." }, "loginExpired": { "message": "انتهت صلاحية جلسة تسجيل الدخول الخاصة بك." }, "logIn": { - "message": "Log in" + "message": "تسجيل الدخول" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "تسجيل الدخول إلى Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "إعادة التسجيل" }, "expiredLink": { - "message": "Expired link" + "message": "رابط منتهي الصَّلاحِيَة" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "الرجاء إعادة التسجيل أو حاول تسجيل الدخول." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "قد يكون لديك حساب بالفعل" }, "logOutConfirmation": { "message": "هل أنت متأكد من أنك تريد تسجيل الخروج؟" @@ -895,10 +888,10 @@ "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزانة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "اجعل حسابك أكثر أمنا من خلال إعداد تسجيل الدخول بخطوتين في تطبيق Bitwarden على الويب." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "هل تريد المتابعة إلى تطبيق الويب؟" }, "editedFolder": { "message": "حُفظ المجلد" @@ -941,7 +934,7 @@ "message": "رابط جديد" }, "addDomain": { - "message": "Add domain", + "message": "أضف نطاق", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "قائمة عناصر الهوية في صفحة التبويب لسهولة الملء التلقائي." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "مسح الحافظة", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1053,7 +1049,7 @@ "message": "إلغاء القفل" }, "additionalOptions": { - "message": "Additional options" + "message": "خيارات إضافية" }, "enableContextMenuItem": { "message": "إظهار خيارات قائمة السياق" @@ -1093,7 +1089,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "التصدير من" }, "exportVault": { "message": "تصدير الخزانة" @@ -1102,13 +1098,13 @@ "message": "صيغة الملف" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "سيكون المِلَفّ المُصدر محميًا بكلمة مرور وسيتطلب كلمة مرور المِلَفّ لفك تشفيره." }, "filePassword": { - "message": "File password" + "message": "كلمة مرور الملف" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "ستُستخدم كلمة المرور هذه لتصدير واستيراد هذا المِلَفّ" }, "accountRestrictedOptionDescription": { "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "الانتقال إلى مؤسسة" }, - "share": { - "message": "مشاركة" - }, "movedItemToOrg": { "message": "$ITEMNAME$ انتقل إلى $ORGNAME$", "placeholders": { @@ -1936,10 +1929,10 @@ "message": "Clear history" }, "nothingToShow": { - "message": "Nothing to show" + "message": "لا يوجد شيء لعرضه" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "لم تقم بتوليد أي شيء مؤخرًا" }, "remove": { "message": "إزالة" @@ -2000,16 +1993,16 @@ "message": "فتح باستخدام رمز PIN" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "تعيين رَقَم التعريف الشخصي" }, "setYourPinButton": { - "message": "Set PIN" + "message": "تعيين رَقَم التعريف الشخصي" }, "setYourPinCode": { "message": "تعيين رمز PIN الخاص بك لإلغاء قفل Bitwarden. سيتم إعادة تعيين إعدادات PIN الخاصة بك إذا قمت بتسجيل الخروج بالكامل من التطبيق." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "سيتم استخدام رَقَم التعريف الشخصي الخاص بك لفتح Bitwarden بدلاً من كلمة المرور الرئيسية. سيتم حذف رَقَم التعريف الشخصي الخاص بك إذا قمت بتسجيل الخروج بالكامل من Bitwarden." }, "pinRequired": { "message": "رمز PIN مطلوب." @@ -2024,7 +2017,7 @@ "message": "فتح باستخدام القياسات الحيوية" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "فتح بكلمة المرور الرئيسية" }, "awaitDesktop": { "message": "في انتظار التأكيد من سطح المكتب" @@ -2036,7 +2029,7 @@ "message": "قفل مع كلمة المرور الرئيسية عند إعادة تشغيل المتصفح" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "أطلب كلمة المرور الرئيسية عند إعادة تشغيل المتصفح" }, "selectOneCollection": { "message": "يجب عليك تحديد مجموعة واحدة على الأقل." @@ -2047,30 +2040,27 @@ "clone": { "message": "استنساخ" }, - "passwordGeneratorPolicyInEffect": { - "message": "واحدة أو أكثر من سياسات المؤسسة تؤثر على إعدادات المولدات الخاصة بك." - }, "passwordGenerator": { - "message": "Password generator" + "message": "مولد كلمة المرور" }, "usernameGenerator": { - "message": "Username generator" + "message": "مولد اسم المستخدم" }, "useThisPassword": { - "message": "Use this password" + "message": "استخدم كلمة المرور هذه" }, "useThisUsername": { - "message": "Use this username" + "message": "استخدم اسم المستخدم هذا" }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "تم توليد كلمة مرور آمنة! لا تنس أن تقوم أيضا بتحديث كلمة المرور الخاصة بك على الموقع." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "استخدام المولد", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "لإنشاء كلمة مرور فريدة قوية", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { @@ -2106,7 +2096,7 @@ "message": "تم استعادة العنصر" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "لديك حساب بالفعل؟" }, "vaultTimeoutLogOutConfirmation": { "message": "سيؤدي تسجيل الخروج إلى إزالة جميع إمكانية الوصول إلى خزانتك ويتطلب المصادقة عبر الإنترنت بعد انتهاء المهلة. هل أنت متأكد من أنك تريد استخدام هذا الإعداد؟" @@ -2118,7 +2108,7 @@ "message": "التعبئة التلقائية والحفظ" }, "fillAndSave": { - "message": "Fill and save" + "message": "عبء ثم احفظ" }, "autoFillSuccessAndSavedUri": { "message": "تم تعبئة العنصر تلقائياً وحفظ عنوان URI" @@ -2199,19 +2189,19 @@ "message": "كلمة المرور الرئيسية الجديدة لا تفي بمتطلبات السياسة العامة." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "احصل على النصائح والإعلانات وفرص البحوث من Bitwarden في صندوق الوارد الخاص بك." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "إلغاء الاشتراك" }, "atAnyTime": { - "message": "at any time." + "message": "في أي وقت." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "عن طريق المتابعة، أنت توافق على" }, "and": { - "message": "and" + "message": "و" }, "acceptPolicies": { "message": "من خلال تحديد هذا المربع فإنك توافق على ما يلي:" @@ -2274,7 +2264,7 @@ "message": "عدم تطابق الحساب" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "عدم تطابق المفتاح الحيوي" }, "nativeMessagingWrongUserKeyDesc": { "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." @@ -2331,9 +2321,12 @@ "message": "An organization policy has blocked importing items into your individual vault." }, "domainsTitle": { - "message": "Domains", + "message": "النطاقات", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "النطاقات المستبعدة" }, @@ -2341,10 +2334,19 @@ "message": "Bitwarden لن يطلب حفظ تفاصيل تسجيل الدخول لهذه النطاقات. يجب عليك تحديث الصفحة حتى تصبح التغييرات سارية المفعول." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "Bitwarden لن يطلب حفظ تفاصيل تسجيل الدخول لهذه النطافات لجميع الحسابات مسجلة الدخول. يجب عليك تحديث الصفحة لكي تصبح التغييرات نافذة المفعول." + }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { - "message": "Website $number$ (URI)", + "message": "الموقع $number$ (URI)", "placeholders": { "number": { "content": "$1", @@ -2361,18 +2363,21 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { - "message": "Excluded domain changes saved" + "message": "تم حفظ تغييرات استبعاد النطاقات" }, "limitSendViews": { "message": "Limit views" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "لا يمكن لأحد عرض هذا الإرسال بعد الوصول إلى الحد الأقصى.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ مشاهدات متبقية", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "نص" }, @@ -2407,22 +2404,15 @@ "message": "ملف" }, "allSends": { - "message": "All Sends", + "message": "كل الإرسالات", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Hide text by default" - }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." + "message": "إخفاء النص بشكل افتراضي" }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "تاريخ الحذف" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "تاريخ انتهاء الصلاحية" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "يوم واحد" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "مُخصّص" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "قبل أن تبدأ" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "لاستخدام منتقي التاريخ على نمط التقويم", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "انقر هنا", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "أن يخرج من النافذة.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "صلاحية تاريخ الانتهاء المقدّم غير صحيح." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "خطأ" }, - "regenerateUsername": { - "message": "إعادة إنشاء اسم المستخدم" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "إنشاء اسم المستخدم" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "نوع اسم المستخدم" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "اسم الموقع الإلكتروني" }, - "whatWouldYouLikeToGenerate": { - "message": "ما الذي ترغب في توليده؟" - }, - "passwordType": { - "message": "نوع كلمة المرور" - }, "service": { "message": "الخدمة" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "استيراد بياناتك إلى Bitwarden؟", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "حماية بياناتك الأخيرة واستيرادك إلى Bitwarden؟", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "حفظ كملف غير مشفر", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "استيراد إلى Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "جارِ الاستيراد...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "لقد تم استيراد البيانات بنجاح!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "خطأ في الاستيراد. تحقق من وحدة التحكم للحصول على التفاصيل.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "تم مواجهة خطأ في الشبكة أثناء الاستيراد.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "الاسم البديل للنطاق" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index aea4e46abeb..8e7d2201f28 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Uzunluq" }, - "passwordMinLength": { - "message": "Minimal parol uzunluğu" - }, "uppercase": { "message": "Böyük hərf (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum simvol" }, - "avoidAmbChar": { - "message": "Anlaşılmaz simvollardan çəkinin", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Anlaşılmaz xarakterlərdən çəkin", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Asan avto-doldurma üçün Vərəq səhifəsində kimlik elementlərini sadalayın." }, + "clickToAutofillOnVault": { + "message": "Seyf görünüşündə avto-doldurmaq üçün elementlərə klikləyin" + }, "clearClipboard": { "message": "Lövhəni təmizlə", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Təşkilata daşı" }, - "share": { - "message": "Paylaş" - }, "movedItemToOrg": { "message": "$ITEMNAME$, $ORGNAME$ şirkətinə daşındı", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Klonla" }, - "passwordGeneratorPolicyInEffect": { - "message": "Bir və ya daha çox təşkilat siyasəti yaradıcı ayarlarınıza təsir edir." - }, "passwordGenerator": { "message": "Parol yaradıcı" }, @@ -2334,6 +2324,9 @@ "message": "Domenlər", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Əngəllənmiş domenlər" + }, "excludedDomains": { "message": "İstisna edilən domenlər" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, giriş etmiş bütün hesablar üçün bu domenlərin giriş detallarını saxlamağı soruşmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." }, + "blockedDomainsDesc": { + "message": "Bu veb saytlar üçün avto-doldurma və digər əlaqəli özəlliklər təklif olunmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." + }, + "autofillBlockedNoticeV2": { + "message": "Bu veb sayt üçün avto-doldurma əngəllənib." + }, + "autofillBlockedNoticeGuidance": { + "message": "Bunu ayarlarda dəyişdir" + }, "websiteItemLabel": { "message": "Veb sayt $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Əngəllənmiş domen dəyişiklikləri saxlanıldı" + }, "excludedDomainsSavedSuccess": { "message": "İstisna domen dəyişikliyi saxlanıldı" }, @@ -2389,14 +2394,6 @@ "message": "Send detalları", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "\"Send\"ləri axtar", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "\"Send\" əlavə et", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Mətn" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Mətni ilkin olaraq gizlət" }, - "maxAccessCountReached": { - "message": "Maksimal müraciət sayına çatıldı", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Müddəti bitib" }, - "pendingDeletion": { - "message": "Silinməsi gözlənilir" - }, "passwordProtected": { "message": "Parolla qorunan" }, @@ -2472,24 +2462,9 @@ "message": "\"Send\"ə düzəliş et", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "\"Send\"in növü nədir?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Bu \"Send\"i açıqlayan bir ad.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Göndərmək istədiyiniz fayl." - }, "deletionDate": { "message": "Silinmə tarixi" }, - "deletionDateDesc": { - "message": "\"Send\" göstərilən tarix və saatda birdəfəlik silinəcək.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send, bu tarixdə həmişəlik silinəcək.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Bitmə tarixi" }, - "expirationDateDesc": { - "message": "Əgər ayarlanıbsa, göstərilən tarix və vaxtda \"Send\"ə müraciət başa çatacaq.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 gün" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Özəl" }, - "maximumAccessCount": { - "message": "Maksimal müraciət sayı" - }, - "maximumAccessCountDesc": { - "message": "Əgər ayarlanıbsa, istifadəçilər maksimal müraciət sayına çatdıqdan sonra bu \"Send\"ə müraciət edə bilməyəcək.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "İstəyinizə görə istifadəçilərdən bu \"Send\"ə müraciət edərkən parol tələb edə bilərsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Alıcıların bu \"Send\"ə müraciət etməsi üçün ixtiyari bir parol əlavə edin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Bu \"Send\" ilə bağlı gizli notlar.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Heç kimin müraciət edə bilməməsi üçün bu \"Send\"i deaktiv et.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Saxladıqdan sonra \"Send\"in keçidini lövhəyə kopyala.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Göndərmək istədiyiniz mətn" - }, - "sendHideText": { - "message": "Bu \"Send\"in mətnini ilkin olaraq gizlət", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Hazırkı müraciət sayı" - }, "createSend": { "message": "Yeni \"Send\"", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Başlamazdan əvvəl" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Təqvim stilində tarix seçici istifadə etmək üçün", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "bura klikləyin", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "yeni bir pəncərə açın.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Göstərilən son istifadə tarixi yararsızdır." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Silinmə və son istifadə tarixlərini saxlayarkən xəta baş verdi." }, - "hideEmail": { - "message": "E-poçt ünvanımı alıcılardan gizlət." - }, "hideYourEmail": { "message": "E-poçt ünvanınız baxanlardan gizlədilsin." }, - "sendOptionsPolicyInEffect": { - "message": "Bir və ya daha çox təşkilat siyasətləri \"Send\" seçimlərinizə təsir edir." - }, "passwordPrompt": { "message": "Ana parolu təkrar soruş" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Xəta" }, - "regenerateUsername": { - "message": "İstifadəçi adını yenidən yarat" + "decryptionError": { + "message": "Şifrə açma xətası" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden, aşağıda sadalanan seyf element(lər)inin şifrəsini aça bilmədi." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Əlavə data itkisini önləmək üçün", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "müştəri dəstəyi ilə əlaqə saxlayın.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "İstifadəçi adı yarat" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "İstifadəçi adı növü" - }, "plusAddressedEmail": { "message": "Üstəgəl ünvanlı e-poçt", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Veb sayt adı" }, - "whatWouldYouLikeToGenerate": { - "message": "Nə yaratmaq istəyirsiniz?" - }, - "passwordType": { - "message": "Parol növü" - }, "service": { "message": "Xidmət" }, @@ -3503,38 +3425,6 @@ "message": "Yığcamlaşdırmanı aç/bağla", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Datanız Bitwarden-ə köçürülsün?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass datanızı qoruyub Bitwarden-ə köçürürsünüz?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Şifrələnməmiş fayl olaraq saxla", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden-ə köçür", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Daxilə köçürülür...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data uğurla daxilə köçürüldü!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Daxilə köçürmə xətası. Detallar üçün konsolu yoxlayın.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Daxilə köçürmə zamanı şəbəkə xətası baş verdi.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domen ləqəbi" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Avto-doldurma təklifləri" }, + "itemSuggestions": { + "message": "Təklif olunan elementlər" + }, "autofillSuggestionsTip": { "message": "Bu saytın avto-doldurması üçün giriş elementini saxlayın" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Mətn \"Send\"ləri" }, - "bitwardenNewLook": { - "message": "Bitwarden-in yeni bir görünüşü var!" - }, - "bitwardenNewLookDesc": { - "message": "Seyf vərəqindən avto-doldurma və axtarış etmə artıq daha asan və intuitivdir. Nəzər salın!" - }, "accountActions": { "message": "Hesab fəaliyyətləri" }, @@ -4677,7 +4564,7 @@ "message": "Uzantı ikonunda giriş üçün avto-doldurma təklif sayını göstər" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Seyfdə cəld kopyalama fəaliyyətlərini göstər" }, "systemDefault": { "message": "İlkin sistem" @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Bu elementə düzəliş etmə icazəniz yoxdur" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Əvvəlcə PIN və ya parol ilə kilid açma tələb olunduğu üçün biometrik ilə kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrik kilid açma indi əlçatmazdır." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Bitwarden masaüstü tətbiqi bağlı olduğu üçün biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Bitwarden masaüstü tətbiqində $EMAIL$ üçün fəal olmadığına görə biometrik kilid açma əlçatmazdır.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Bilinməyən bilinməyən bir səbəbə görə biometrik kilid açma əlçatmazdır." + }, "authenticating": { "message": "Kimlik doğrulama" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 37b234105c3..57496e83f41 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Даўжыня" }, - "passwordMinLength": { - "message": "Мінімальная даўжыня пароля" - }, "uppercase": { "message": "Вялікія літары (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Мінімум спецыяльных сімвалаў" }, - "avoidAmbChar": { - "message": "Пазбягаць неадназначных сімвалаў", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Пазбягаць неадназначных сімвалаў", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Спіс элементаў пасведчання на старонцы з укладкамі для лёгкага аўтазапаўнення." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Ачыстка буфера абмену", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Перамясціць у арганізацыю" }, - "share": { - "message": "Абагуліць" - }, "movedItemToOrg": { "message": "$ITEMNAME$ перамешчана ў $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Кланіраваць" }, - "passwordGeneratorPolicyInEffect": { - "message": "Адна або больш палітык арганізацыі ўплывае на налады генератара." - }, "passwordGenerator": { "message": "Генератар пароляў" }, @@ -2334,6 +2324,9 @@ "message": "Дамены", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Выключаныя дамены" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Вэб-сайт $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Падрабязнасці Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Пошук у Send'ах", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Дадаць Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Тэкст" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Дасягнута максімальная колькасць доступаў", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Пратэрмінавана" }, - "pendingDeletion": { - "message": "Чакаецца выдаленне" - }, "passwordProtected": { "message": "Абаронена паролем" }, @@ -2472,24 +2462,9 @@ "message": "Рэдагаваць Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Які гэта тып Send'a?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Зразумелая назва для апісання гэтага Send'a.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Файл, які вы хочаце адправіць." - }, "deletionDate": { "message": "Дата выдалення" }, - "deletionDateDesc": { - "message": "Send будзе незваротна выдалены ў азначаныя дату і час.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Дата завяршэння" }, - "expirationDateDesc": { - "message": "Калі зададзена, то доступ да гэтага Send міне ў азначаную дату і час.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 дзень" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Карыстальніцкі" }, - "maximumAccessCount": { - "message": "Максімальная колькасць доступаў" - }, - "maximumAccessCountDesc": { - "message": "Калі прызначана, то карыстальнікі больш не змогуць атрымаць доступ да гэтага Send пасля таго, як будзе дасягнута максімальная колькасць зваротаў.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Па магчымасці запытваць у карыстальнікаў пароль для доступу да гэтага Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Прыватныя нататкі пра гэты Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Адключыць гэты Send, каб ніхто не змог атрымаць да яго доступ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Скапіяваць спасылку на гэты Send у буфер абмену пасля захавання.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Тэкст, які вы хочаце адправіць." - }, - "sendHideText": { - "message": "Прадвызначана хаваць тэкст гэтага Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Бягучая колькасць доступаў" - }, "createSend": { "message": "Стварыць новы Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Перад тым, як пачаць" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Для выкарыстання каляндарнага стылю выбару даты", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "націсніце тут", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "для адкрыцця ў новым акне.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Азначаная дата завяршэння тэрміну дзеяння з'яўляецца няправільнай." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Адбылася памылка пры захаванні дат выдалення і завяршэння тэрміну дзеяння." }, - "hideEmail": { - "message": "Схаваць мой адрас электроннай пошты ад атрымальнікаў." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Адна або больш палітык арганізацыі ўплываюць на параметры Send." - }, "passwordPrompt": { "message": "Паўторны запыт асноўнага пароля" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Памылка" }, - "regenerateUsername": { - "message": "Паўторна генерыраваць імя карыстальніка" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Генерыраваць імя карыстальніка" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Тып імя карыстальніка" - }, "plusAddressedEmail": { "message": "Адрасы электроннай пошты з плюсам", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Назва вэб-сайта" }, - "whatWouldYouLikeToGenerate": { - "message": "Што вы хочаце генерыраваць?" - }, - "passwordType": { - "message": "Тып пароля" - }, "service": { "message": "Сэрвіс" }, @@ -3503,38 +3425,6 @@ "message": "Згарнуць/Разгарнуць", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Імпартаванне...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Даныя паспяхова імпартаваны!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Мянушка дамена" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 554650d5800..6e9f992f5ba 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Дължина" }, - "passwordMinLength": { - "message": "Минимална дължина на паролата" - }, "uppercase": { "message": "Главни букви (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Минимален брой специални знаци" }, - "avoidAmbChar": { - "message": "Без нееднозначни знаци", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Без нееднозначни знаци", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Показване на самоличностите в страницата с разделите, за лесно автоматично попълване." }, + "clickToAutofillOnVault": { + "message": "Щракнете върху елементите в трезора за автоматично попълване" + }, "clearClipboard": { "message": "Изчистване на буфера", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Преместване в организация" }, - "share": { - "message": "Споделяне" - }, "movedItemToOrg": { "message": "$ITEMNAME$ се премести в $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Клониране" }, - "passwordGeneratorPolicyInEffect": { - "message": "Поне една политика на организация влияе на настройките на генерирането на паролите." - }, "passwordGenerator": { "message": "Генератор на пароли" }, @@ -2334,6 +2324,9 @@ "message": "Домейни", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Блокирани домейни" + }, "excludedDomains": { "message": "Изключени домейни" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Битуорден няма да пита дали да запазва данните за вход в тези сайтове за всички регистрации, в които сте вписан(а). За да влезе правилото в сила, презаредете страницата." }, + "blockedDomainsDesc": { + "message": "Автоматичното попълване и други свързани функции няма да бъдат предлагани за тези уеб сайтове. Трябва да презаредите страницата, за да влязат в сила промените." + }, + "autofillBlockedNoticeV2": { + "message": "Автоматичното попълване е блокирано за този уеб сайт." + }, + "autofillBlockedNoticeGuidance": { + "message": "Променете това в настройките" + }, "websiteItemLabel": { "message": "Уеб сайт $number$ (адрес)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Промените на блокираните домейни са запазени" + }, "excludedDomainsSavedSuccess": { "message": "Промените на изключените домейни са запазени" }, @@ -2389,14 +2394,6 @@ "message": "Подробности за Изпращането", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Търсене в изпратените", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Добавяне на изпращане", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Текст" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Скриване на текста по подразбиране" }, - "maxAccessCountReached": { - "message": "Достигнат е максималният брой достъпвания", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Изтекъл" }, - "pendingDeletion": { - "message": "Предстои изтриване" - }, "passwordProtected": { "message": "Защита с парола" }, @@ -2472,24 +2462,9 @@ "message": "Редактиране на изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Вид на изпратеното", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Описателно име за това изпращане.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Файл за изпращане." - }, "deletionDate": { "message": "Дата на изтриване" }, - "deletionDateDesc": { - "message": "Изпращането ще бъде окончателно изтрито на зададената дата и време.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Изпращането ще бъде окончателно изтрито на тази дата.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Срок на валидност" }, - "expirationDateDesc": { - "message": "При задаване — това изпращане ще се изключи на зададената дата и време.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 ден" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "По избор" }, - "maximumAccessCount": { - "message": "Максимален брой достъпвания." - }, - "maximumAccessCountDesc": { - "message": "При задаване — това изпращане ще се изключи след определен брой достъпвания.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Изискване на парола за достъп до това изпращане.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Добавете незадължителна парола, с която получателите да имат достъп до това Изпращане.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Скрити бележки за това изпращане.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Пълно спиране на това изпращане — никой няма да има достъп.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Копиране на връзката към това изпращане при запазването му.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Текст за изпращане." - }, - "sendHideText": { - "message": "Стандартно текстът на това изпращане да се крие.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Текущ брой на достъпванията" - }, "createSend": { "message": "Създаване на изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Преди да почнете" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "За избор на дата от каландар", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "натиснете тук", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "за изскачащ прозорец.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Неправилна дата на валидност." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Грешка при запазване на датата на валидност и изтриване." }, - "hideEmail": { - "message": "Скриване на е-пощата ми от получателите." - }, "hideYourEmail": { "message": "Скриване на Вашата е-поща от получателите." }, - "sendOptionsPolicyInEffect": { - "message": "Поне една политика на организация влияе на настройките за изпращане." - }, "passwordPrompt": { "message": "Повторно запитване за главната парола" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Грешка" }, - "regenerateUsername": { - "message": "Повторно генериране на потр. име" + "decryptionError": { + "message": "Грешка при дешифриране" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Битоурден не може да дешифрира елементите от трезора посочени по-долу." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Свържете се с поддръжката", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "за да избегнете загубата на данни.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Генериране на потр. име" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Тип потребителско име" - }, "plusAddressedEmail": { "message": "Адрес на е-поща с плюс", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Име на уеб сайт" }, - "whatWouldYouLikeToGenerate": { - "message": "Какво бихте искали да генерирате?" - }, - "passwordType": { - "message": "Тип парола" - }, "service": { "message": "Услуга" }, @@ -3503,38 +3425,6 @@ "message": "Превключване на свиването", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Внасяне на данните Ви в Битуорден?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Защитаване на данните Ви от LastPass и внасяне в Битуорден?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Запазване като нешифрован файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Внасяне в Битуорден", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Внасяне…", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Данните бяха внесени успешно!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Грешка при внасянето. Вижте конзолата за подробности.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "По време на внасянето възникна мрежова грешка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдонимен домейн" }, @@ -4086,7 +3976,10 @@ "message": "Секретният ключ е премахнат" }, "autofillSuggestions": { - "message": "Автоматично попълване на предложения" + "message": "Предложения за авт. попълване" + }, + "itemSuggestions": { + "message": "Препоръчани елементи" }, "autofillSuggestionsTip": { "message": "Запазване на елемент за вписване за този уеб сайт, за авт. попълване" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Текстови изпращания" }, - "bitwardenNewLook": { - "message": "Биуорден има нов облик!" - }, - "bitwardenNewLookDesc": { - "message": "Сега е по-лесно и интуитивно от всякога да използвате автоматичното попълване и да търсите в раздела на трезора. Разгледайте!" - }, "accountActions": { "message": "Действия по регистрацията" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Нямате право за редактиране на този елемент" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Отключването с биометрични данни не е налично, тъй като първо се изисква отключване чрез ПИН или парола." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Отключването с биометрични данни не е налично в момента." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Отключването с биометрични данни не е налично, тъй като приложението на Биуорден за компютър не работи." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Отключването с биометрични данни не е налично, тъй като не е включено за $EMAIL$ в приложението на Битуорден за компютър.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Отключването с биометрични данни не е налично по неизвестна причина." + }, "authenticating": { "message": "Удостоверяване" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index c31f3511a5f..219071238e9 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -454,9 +454,6 @@ "length": { "message": "দৈর্ঘ্য" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "ন্যূনতম বিশেষ" }, - "avoidAmbChar": { - "message": "অস্পষ্ট বর্ণগুলি এড়িয়ে চলুন", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "ক্লিপবোর্ড পরিষ্কার", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "ভাগ করুন" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "নকল" }, - "passwordGeneratorPolicyInEffect": { - "message": "এক বা একাধিক সংস্থার নীতিগুলি আপনার উৎপাদকের সেটিংসকে প্রভাবিত করছে।" - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "অবসায়িত" }, - "pendingDeletion": { - "message": "মুছে ফেলার জন্য অপেক্ষমান" - }, "passwordProtected": { "message": "পাসওয়ার্ড সুরক্ষিত" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index d6cb591a524..81c162e91be 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekst" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Isteklo" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dan" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index e405a18f885..89ab22c1794 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -20,16 +20,16 @@ "message": "Crea un compte" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nou a Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Inicieu sessió amb la clau de pas" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Inici de sessió únic" }, "welcomeBack": { - "message": "Welcome back" + "message": "Benvingut/da de nou" }, "setAStrongPassword": { "message": "Estableix una contrasenya segura" @@ -120,7 +120,7 @@ "message": "Copia contrasenya" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copia frase de pas" }, "copyNote": { "message": "Copia nota" @@ -177,7 +177,7 @@ "message": "Copia notes" }, "fill": { - "message": "Fill", + "message": "Emplena", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "Emplena automàticament l'identitat" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Emplena el codi de verificació" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Emplena el codi de verificació", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -443,7 +443,7 @@ "message": "Genera contrasenya" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Genera frase de pas" }, "regeneratePassword": { "message": "Regenera contrasenya" @@ -454,9 +454,6 @@ "length": { "message": "Longitud" }, - "passwordMinLength": { - "message": "Longitud mínima de la contrasenya" - }, "uppercase": { "message": "Majúscula (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Mínim de caràcters especials" }, - "avoidAmbChar": { - "message": "Eviteu caràcters ambigus", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Eviteu caràcters ambigus", "description": "Label for the avoid ambiguous characters checkbox." @@ -583,7 +576,7 @@ "message": "Notes" }, "privateNote": { - "message": "Private note" + "message": "Nota privada" }, "note": { "message": "Nota" @@ -689,7 +682,7 @@ "message": "Temps d'espera de la caixa forta" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Temps d'espera" }, "lockNow": { "message": "Bloqueja ara" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Llista els elements d'identitat de la pestanya de la pàgina per facilitar l'autoemplenat." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Buida el porta-retalls", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Desplaça a l'organització" }, - "share": { - "message": "Comparteix" - }, "movedItemToOrg": { "message": "$ITEMNAME$ desplaçat a $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clona" }, - "passwordGeneratorPolicyInEffect": { - "message": "Una o més polítiques d’organització afecten la configuració del generador." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Dominis", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Dominis exclosos" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no demanarà que es guarden les dades d'inici de sessió d'aquests dominis per a tots els comptes iniciats. Heu d'actualitzar la pàgina perquè els canvis tinguen efecte." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2386,15 +2391,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Detalls de l'enviament", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "searchSends": { - "message": "Cerca Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Afig Send", + "message": "Detalls del Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -2411,18 +2408,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Ocultar el text per defecte" - }, - "maxAccessCountReached": { - "message": "S'ha assolit el recompte màxim d'accesos", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." + "message": "Amaga el text per defecte" }, "expired": { "message": "Caducat" }, - "pendingDeletion": { - "message": "Pendent de supressió" - }, "passwordProtected": { "message": "Protegit amb contrasenya" }, @@ -2472,35 +2462,16 @@ "message": "Edita Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Quin tipus de Send és aquest?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Un nom apropiat per descriure aquest Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "El fitxer que voleu enviar." - }, "deletionDate": { "message": "Data de supressió" }, - "deletionDateDesc": { - "message": "L'enviament se suprimirà permanentment a la data i hora especificades.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { - "message": "L'enviament s'esborrarà permanentment en aquesta data.", + "message": "El Send se suprimirà permanentment en aquesta data.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { "message": "Data de caducitat" }, - "expirationDateDesc": { - "message": "Si s'estableix, l'accés a aquest enviament caducarà en la data i hora especificades.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dia" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Personalitzat" }, - "maximumAccessCount": { - "message": "Recompte màxim d'accessos" - }, - "maximumAccessCountDesc": { - "message": "Si s’estableix, els usuaris ja no podran accedir a aquest Send una vegada s’assolisca el nombre màxim d’accessos.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Opcionalment, necessiteu una contrasenya perquè els usuaris accedisquen a aquest Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Notes privades sobre aquest Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Desactiveu aquest Send perquè ningú no hi puga accedir.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copieu l'enllaç d'aquest Send al porta-retalls després de guardar-lo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "El text que voleu enviar." - }, - "sendHideText": { - "message": "Amaga el text d'aquest Send per defecte.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Recompte d’accessos actual" - }, "createSend": { "message": "Crea un nou Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Abans de començar" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Per utilitzar un selector de dates d'estil calendari", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "feu clic ací", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "per eixir de la finestra.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "La data de caducitat proporcionada no és vàlida." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "S'ha produït un error en guardar les dates de supressió i caducitat." }, - "hideEmail": { - "message": "Amagueu la meua adreça de correu electrònic als destinataris." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Una o més polítiques d'organització afecten les vostres opcions del Send." - }, "passwordPrompt": { "message": "Sol·licitud de la contrasenya mestra" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenera el nom d'usuari" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Genera un nom d'usuari" @@ -2918,7 +2849,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Utilitzeu paraules $RECOMMENDED$ o més per generar una frase de pas segura.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Tipus de nom d'usuari" - }, "plusAddressedEmail": { "message": "Adreça amb sufix", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nom del lloc web" }, - "whatWouldYouLikeToGenerate": { - "message": "Què voleu generar?" - }, - "passwordType": { - "message": "Tipus de contrasenya" - }, "service": { "message": "Servei" }, @@ -3503,38 +3425,6 @@ "message": "Redueix/Amplia", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Voleu importar les vostres dades a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protegiu les vostres dades de LastPass i importeu-les a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guarda com a fitxer sense xifrar", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importa a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "S'està important...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Les dades s'han importat correctament!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "S'ha produït un error en importar. Consulteu la consola per obtenir més informació.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "S'ha trobat un error de xarxa durant la importació.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alies de domini" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index cf3ae3e2fd0..0a05974279a 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Délka" }, - "passwordMinLength": { - "message": "Minimální délka hesla" - }, "uppercase": { "message": "Velká písmena (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimální počet speciálních znaků" }, - "avoidAmbChar": { - "message": "Nepoužívat zaměnitelné znaky", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Nepoužívat zaměnitelné znaky", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Pro snadné vyplnění zobrazí položky identit na obrazovce Karta." }, + "clickToAutofillOnVault": { + "message": "Klepněte na položky pro automatické vyplnění v zobrazení trezoru" + }, "clearClipboard": { "message": "Vymazat schránku", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Přesunout do organizace" }, - "share": { - "message": "Sdílet" - }, "movedItemToOrg": { "message": "$ITEMNAME$ přesunut do $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Duplikovat" }, - "passwordGeneratorPolicyInEffect": { - "message": "Jedna nebo více zásad organizace ovlivňují nastavení generátoru." - }, "passwordGenerator": { "message": "Generátor hesla" }, @@ -2334,6 +2324,9 @@ "message": "Domény", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokované domény" + }, "excludedDomains": { "message": "Vyloučené domény" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude žádat o uložení přihlašovacích údajů pro tyto domény pro všechny přihlášené účty. Aby se změny projevily, musíte stránku obnovit." }, + "blockedDomainsDesc": { + "message": "Automatické vyplňování a další související funkce nebudou pro tyto webové stránky nabízeny. Aby se změny projevily, musíte stránku aktualizovat." + }, + "autofillBlockedNoticeV2": { + "message": "Automatické vyplňování je pro tuto stránku zablokováno." + }, + "autofillBlockedNoticeGuidance": { + "message": "Změňte to v nastavení" + }, "websiteItemLabel": { "message": "Webová stránka $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Změny v zablokovaných doménách byly uloženy" + }, "excludedDomainsSavedSuccess": { "message": "Vyloučené změny domény byly uloženy" }, @@ -2389,14 +2394,6 @@ "message": "Podrobnosti Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Prohledat Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Přidat Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Ve výchozím nastavení skrýt text" }, - "maxAccessCountReached": { - "message": "Dosažen maximální počet přístupů", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Vypršela platnost" }, - "pendingDeletion": { - "message": "Čekání na smazání" - }, "passwordProtected": { "message": "Chráněno heslem" }, @@ -2472,24 +2462,9 @@ "message": "Upravit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Jakého typu je tento Send?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Přátelský název pro popis tohoto Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Soubor, který chcete odeslat." - }, "deletionDate": { "message": "Datum smazání" }, - "deletionDateDesc": { - "message": "Tento Send bude trvale smazán v určený datum a čas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Tento Send bude trvale smazán v určené datum.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Datum vypršení platnosti" }, - "expirationDateDesc": { - "message": "Je-li nastaveno, přístup k tomuto Send vyprší v daný datum a čas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 den" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Vlastní" }, - "maximumAccessCount": { - "message": "Maximální počet přístupů" - }, - "maximumAccessCountDesc": { - "message": "Je-li nastaveno, uživatelé již nebudou mít přístup k tomuto Send, jakmile bude dosaženo maximálního počtu přístupů.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Volitelně vyžadovat heslo pro přístup k tomuto Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Přidá volitelné heslo pro příjemce pro přístup k tomuto Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Soukromé poznámky o tomto Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deaktivuje tento Send, díky čemuž k němu nebude moci nikdo přistoupit.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Zkopíruje odkaz pro sdílení tohoto Send po uložení.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Text, který chcete odeslat." - }, - "sendHideText": { - "message": "Skrýt ve výchozím stavu text tohoto Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Aktuální počet přístupů" - }, "createSend": { "message": "Nový Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Než začnete" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Chcete-li použít k výběru data styl kalendáře", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klepněte zde", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "pro zobrazení okna.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Uvedené datum vypršení platnosti není platné." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Došlo k chybě při ukládání datumu smzání a vypršení platnosti." }, - "hideEmail": { - "message": "Skrýt mou e-mailovou adresu před příjemci." - }, "hideYourEmail": { "message": "Skryje Vaši e-mailovou adresu před zobrazením." }, - "sendOptionsPolicyInEffect": { - "message": "Jedna nebo více zásad organizace ovlivňuje nastavení Send." - }, "passwordPrompt": { "message": "Zeptat se znovu na hlavní heslo" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Chyba" }, - "regenerateUsername": { - "message": "Znovu vygenerovat uživatelské jméno" + "decryptionError": { + "message": "Chyba dešifrování" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nemohl dešifrovat níže uvedené položky v trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznickou podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "abyste zabránili ztrátě dat.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Vygenerovat uživatelské jméno" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Typ uživatelského jména" - }, "plusAddressedEmail": { "message": "E-mailová adresa s plusem", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Název webu" }, - "whatWouldYouLikeToGenerate": { - "message": "Co chcete vygenerovat?" - }, - "passwordType": { - "message": "Typ hesla" - }, "service": { "message": "Služba" }, @@ -3503,38 +3425,6 @@ "message": "Přepnout sbalení", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importovat data do Bitwardenu?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Chránit Vaše data LastPass a importovat do Bitwardenu?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Uložit jako nešifrovaný soubor", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importovat do Bitwardenu", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importování...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data byla úspěšně importována!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Chyba při importu. Podrobnosti naleznete v konzoli.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Při importu došlo k chybě sítě.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Doména aliasu" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Návrhy automatického vyplňování" }, + "itemSuggestions": { + "message": "Navrhované položky" + }, "autofillSuggestionsTip": { "message": "Uložit přihlašovací údaje pro tuto stránku do automatického vyplňování" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Sends s texty" }, - "bitwardenNewLook": { - "message": "Bitwarden má nový vzhled!" - }, - "bitwardenNewLookDesc": { - "message": "Je snazší a intuitivnější než kdy jindy automaticky vyplňovat a vyhledávat z karty trezor. Mrkněte se!" - }, "accountActions": { "message": "Činnosti účtu" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Nemáte oprávnění upravit tuto položku" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrické odemknutí je nedostupné, protože je potřeba nejprve odemknout pomocí PIN nebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrické odemknutí je momentálně nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrické odemknutí není dostupné, protože je aplikace Bitwarden zavřena." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrické odemknutí není dostupné, protože není povoleno pro $EMAIL$ v desktopové aplikaci Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrické odemknutí je momentálně z neznámého důvodu nedostupné." + }, "authenticating": { "message": "Ověřování" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 3d4f1c73c02..cbaa31fea30 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Hyd" }, - "passwordMinLength": { - "message": "Hyd lleiaf cyfrineiriau" - }, "uppercase": { "message": "Priflythrennau (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Isafswm nodau arbennig" }, - "avoidAmbChar": { - "message": "Osgoi nodau amwys", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clirio'r clipfwrdd", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Rhannu" - }, "movedItemToOrg": { "message": "Symudwyd $ITEMNAME$ i $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Parthau wedi'u heithrio" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Chwilio drwy Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Ychwanegu Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Testun" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Wedi dod i ben" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Dyddiad dileu" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Dyddiad dod i ben" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 diwrnod" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Addasedig" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Cyn i chi ddechrau" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "cliciwch yma", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Dyw'r dyddiad dod i ben a roddwyd ddim yn ddilys." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Cuddio fy nghyfeiriad ebost rhag derbynwyr." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Ailofyn am y prif gyfrinair" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Gwall" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Cynhyrchu enw defnyddiwr" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Math o enw defnyddiwr" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "Beth hoffech chi ei gynhyrchu?" - }, - "passwordType": { - "message": "Math o gyfrinair" - }, "service": { "message": "Gwasanaeth" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Hoffech chi fewnforio'ch data i Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Yn mewnforio...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Cafodd y data ei fewnforio'n llwyddiannus!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 207f409378e..628ce983c09 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Længde" }, - "passwordMinLength": { - "message": "Minimumslængde på adgangskode" - }, "uppercase": { "message": "Store bogstaver (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Mindste antal specialtegn" }, - "avoidAmbChar": { - "message": "Undgå tvetydige tegn", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Undgå tvetydige tegn", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Vis identitetsemner på siden Fane for nem autoudfyldning." }, + "clickToAutofillOnVault": { + "message": "Klik på emner for at autoudfylde i Boks-visning" + }, "clearClipboard": { "message": "Ryd udklipsholder", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Flyt til organisation" }, - "share": { - "message": "Del" - }, "movedItemToOrg": { "message": "$ITEMNAME$ flyttet til $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Klon" }, - "passwordGeneratorPolicyInEffect": { - "message": "Én eller flere organisationspolitikker påvirker dine generatorindstillinger." - }, "passwordGenerator": { "message": "Adgangskodegenerator" }, @@ -2334,6 +2324,9 @@ "message": "Domæner", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokerede domæner" + }, "excludedDomains": { "message": "Ekskluderede domæner" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden vil ikke anmode om at gemme login-detaljer for disse domæner for alle indloggede konti. Siden skal opfriskes for at effektuere ændringerne." }, + "blockedDomainsDesc": { + "message": "Autofyldning og andre relaterede funktioner tilbydes ikke på disse websteder. Siden skal opdateres for at effektuere ændringerne." + }, + "autofillBlockedNoticeV2": { + "message": "Autoudfyldning blokeret for dette websted." + }, + "autofillBlockedNoticeGuidance": { + "message": "Ændr dette i Indstillinger" + }, "websiteItemLabel": { "message": "Websted $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blokeret domæne-ændringer gemt" + }, "excludedDomainsSavedSuccess": { "message": "Ekskluderet domæne-ændringer gemt" }, @@ -2389,14 +2394,6 @@ "message": "Send-detaljer", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Søg i Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Tilføj Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekst" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Skjul tekst som standard" }, - "maxAccessCountReached": { - "message": "Maksimalt adgangsantal nået", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Udløbet" }, - "pendingDeletion": { - "message": "Afventer sletning" - }, "passwordProtected": { "message": "Kodeordsbeskyttet" }, @@ -2472,24 +2462,9 @@ "message": "Redigér Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Hvilken type Send er denne?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Et venligt navn til at beskrive denne Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Den fil, du vil sende." - }, "deletionDate": { "message": "Sletningsdato" }, - "deletionDateDesc": { - "message": "Send'en slettes permanent på den angivne dato og tid.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Denne Send slettes permanent på denne dato.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Udløbsdato" }, - "expirationDateDesc": { - "message": "Hvis angivet, vil adgangen til denne Send udløbe på den angivne dato og tidspunkt.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dag" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Tilpasset" }, - "maximumAccessCount": { - "message": "Maksimalt antal tilgange" - }, - "maximumAccessCountDesc": { - "message": "Hvis opsat, vil brugere ikke længere kunne tilgå denne Send, når det maksimale adgangsantal er nået.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Valgfrit brugeradgangskodekrav for at tilgå denne Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Tilføj en valgfri adgangskode til modtagere for adgang til denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notater om denne Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deaktivér denne Send, så ingen kan tilgå den.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopiér denne Sends link til udklipsholderen når du gemmer.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Den tekst, du vil sende." - }, - "sendHideText": { - "message": "Skjul denne Sends tekst som standard.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Aktuelt antal tilgange" - }, "createSend": { "message": "Ny Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Før du starter" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "For at bruge en datovælger i kalenderstil skal du", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klikke her", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "for at åbne dit vindue.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Den angivne udløbsdato er ikke gyldig." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Der opstod en fejl under forsøget på at gemme dine sletnings- og udløbsdatoer." }, - "hideEmail": { - "message": "Skjul min e-mailadresse for modtagere." - }, "hideYourEmail": { "message": "Skjul e-mailadressen for modtagere." }, - "sendOptionsPolicyInEffect": { - "message": "Én eller flere organisationspolitikker påvirker dine Send-valgmuligheder." - }, "passwordPrompt": { "message": "Genanmodning om hovedadgangskode" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Fejl" }, - "regenerateUsername": { - "message": "Regenerér brugernavn" + "decryptionError": { + "message": "Dekrypteringsfejl" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kunne ikke dekryptere boks-emne(r) anført nedenfor." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontakt kundeservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "for at undgå yderligere tab af data.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generér brugernavn" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Brugernavnstype" - }, "plusAddressedEmail": { "message": "Plus-adresseret e-mail", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Hjemmeside navn" }, - "whatWouldYouLikeToGenerate": { - "message": "Hvad vil du gerne generere?" - }, - "passwordType": { - "message": "Adgangskodetype" - }, "service": { "message": "Tjeneste" }, @@ -3503,38 +3425,6 @@ "message": "Fold sammen/ud", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importér data til Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Beskyt LastPass-data og importér dem til Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Gem som ukrypteret fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importér til Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerer...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data er nu importeret!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fejl under import. Tjek konsollen for detaljer.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netværksfejl opstod under import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomæne" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autoudfyldningsforslag" }, + "itemSuggestions": { + "message": "Foreslåede emner" + }, "autofillSuggestionsTip": { "message": "Gem et loginemne for dette websted til autoudfyldning" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Tekst-Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden har fået et nyt look!" - }, - "bitwardenNewLookDesc": { - "message": "Det er lettere og mere intuitivt end nogensinde at autoudfylde og søge via fanen Boks. Tag et kig omkring!" - }, "accountActions": { "message": "Kontohandlinger" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Ingen tilladelse til at redigere dette emne" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisk oplåsning er utilgængelig, da PIN- eller adgangskode kræves først." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisk oplåsning er p.t. utilgængelig." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisk oplåsning er utilgængelig, da Bitwarden-appen er lukket." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisk oplåsning er utilgængelig, da det ikke er aktiveret for $EMAIL$ i Bitwarden computer-appen.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisk oplåsning er p.t. utilgængelig grundet en ukendt årsag." + }, "authenticating": { "message": "Godkender" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 169cdf6aa78..86d9d513e40 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -193,10 +193,10 @@ "message": "Identität automatisch ausfüllen" }, "fillVerificationCode": { - "message": "Verifizierungscode eingeben" + "message": "Verifizierungscode ausfüllen" }, "fillVerificationCodeAria": { - "message": "Verifizierungscode eingeben", + "message": "Verifizierungscode ausfüllen", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -454,9 +454,6 @@ "length": { "message": "Länge" }, - "passwordMinLength": { - "message": "Minimale Passwortlänge" - }, "uppercase": { "message": "Großbuchstaben (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Mindestanzahl Sonderzeichen" }, - "avoidAmbChar": { - "message": "Mehrdeutige Zeichen vermeiden", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Mehrdeutige Zeichen vermeiden", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Identitäten-Einträge auf der Tab Seite anzeigen, um das Auto-Ausfüllen zu vereinfachen." }, + "clickToAutofillOnVault": { + "message": "Klicke auf Einträge zum automatischen Ausfüllen in der Tresor-Ansicht" + }, "clearClipboard": { "message": "Zwischenablage leeren", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "In Organisation verschieben" }, - "share": { - "message": "Teilen" - }, "movedItemToOrg": { "message": "$ITEMNAME$ verschoben nach $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Duplizieren" }, - "passwordGeneratorPolicyInEffect": { - "message": "Eine oder mehrere Organisationsrichtlinien beeinflussen deine Generator-Einstellungen." - }, "passwordGenerator": { "message": "Passwort-Generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Gesperrte Domains" + }, "excludedDomains": { "message": "Ausgeschlossene Domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden wird für alle angemeldeten Konten nicht danach fragen Zugangsdaten für diese Domains speichern. Du musst die Seite neu laden, damit die Änderungen wirksam werden." }, + "blockedDomainsDesc": { + "message": "Automatisches Ausfüllen und andere zugehörige Funktionen werden für diese Webseiten nicht angeboten. Du musst die Seite neu laden, damit die Änderungen wirksam werden." + }, + "autofillBlockedNoticeV2": { + "message": "Automatisches Ausfüllen ist für diese Website gesperrt." + }, + "autofillBlockedNoticeGuidance": { + "message": "Dies in den Einstellungen ändern" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Änderungen gesperrter Domains gespeichert" + }, "excludedDomainsSavedSuccess": { "message": "Änderungen der ausgeschlossenen Domain gespeichert" }, @@ -2389,14 +2394,6 @@ "message": "Send-Details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Sends suchen", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Send hinzufügen", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Text standardmäßig ausblenden" }, - "maxAccessCountReached": { - "message": "Maximale Zugriffsanzahl erreicht", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Abgelaufen" }, - "pendingDeletion": { - "message": "Ausstehende Löschung" - }, "passwordProtected": { "message": "Passwortgeschützt" }, @@ -2472,24 +2462,9 @@ "message": "Send bearbeiten", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Welche Art von Send ist das?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Ein eigener Name, um dieses Send zu beschreiben.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Die Datei, die du senden möchtest." - }, "deletionDate": { "message": "Löschdatum" }, - "deletionDateDesc": { - "message": "Das Send wird am angegebenen Datum zur angegebenen Uhrzeit dauerhaft gelöscht.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Das Send wird an diesem Datum dauerhaft gelöscht.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Ablaufdatum" }, - "expirationDateDesc": { - "message": "Falls aktiviert, verfällt der Zugriff auf dieses Send am angegebenen Datum zur angegebenen Uhrzeit.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 Tag" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Benutzerdefiniert" }, - "maximumAccessCount": { - "message": "Maximale Zugriffsanzahl" - }, - "maximumAccessCountDesc": { - "message": "Falls aktiviert, können Benutzer nicht mehr auf dieses Send zugreifen, sobald die maximale Zugriffsanzahl erreicht ist.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optional ein Passwort verlangen, damit Benutzer auf dieses Send zugreifen können.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Füge ein optionales Passwort hinzu, mit dem Empfänger auf dieses Send zugreifen können.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private Notizen zu diesem Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Dieses Send deaktivieren, damit niemand darauf zugreifen kann.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopiere den Send-Link beim Speichern in die Zwischenablage.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Der Text, den du senden möchtest." - }, - "sendHideText": { - "message": "Send-Text standardmäßig ausblenden.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Aktuelle Zugriffsanzahl" - }, "createSend": { "message": "Neues Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Bevor du beginnst" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Um einen Kalender-Datumsauswahl zu verwenden", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "hier klicken", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "zum Öffnen in einem neuen Fenster.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Das angegebene Verfallsdatum ist nicht gültig." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Es gab einen Fehler beim Speichern deiner Lösch- und Verfallsdaten." }, - "hideEmail": { - "message": "Meine E-Mail-Adresse vor den Empfängern ausblenden." - }, "hideYourEmail": { "message": "Verberge deine E-Mail-Adresse vor Betrachtern." }, - "sendOptionsPolicyInEffect": { - "message": "Eine oder mehrere Organisationsrichtlinien beeinflussen deine Send Einstellungen." - }, "passwordPrompt": { "message": "Master-Passwort erneut abfragen" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Fehler" }, - "regenerateUsername": { - "message": "Benutzername neu generieren" + "decryptionError": { + "message": "Entschlüsselungsfehler" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden konnte folgende(n) Tresor-Eintrag/Einträge nicht entschlüsseln." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktiere den Kundensupport", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": ", um zusätzlichen Datenverlust zu vermeiden.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Benutzername generieren" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Benutzernamenstyp" - }, "plusAddressedEmail": { "message": "Plus-adressierte E-Mail-Adresse", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Websitename" }, - "whatWouldYouLikeToGenerate": { - "message": "Was möchtest du generieren?" - }, - "passwordType": { - "message": "Passworttyp" - }, "service": { "message": "Dienst" }, @@ -3503,38 +3425,6 @@ "message": "Ein-/ausklappen", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Deine Daten in Bitwarden importieren?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Deine LastPass-Daten schützen und in Bitwarden importieren?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Als unverschlüsselte Datei speichern", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "In Bitwarden importieren", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Wird importiert...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Daten erfolgreich importiert!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fehler beim Importieren. Überprüfe die Konsole für Details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netzwerkfehler beim Importieren.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias-Domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Vorschläge zum Auto-Ausfüllen" }, + "itemSuggestions": { + "message": "Vorgeschlagene Einträge" + }, "autofillSuggestionsTip": { "message": "Speichere einen Login-Eintrag für diese Seite zum automatischen Ausfüllen" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text-Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden hat einen neuen Look!" - }, - "bitwardenNewLookDesc": { - "message": "Auto-Ausfüllen und Suchen vom Tresor-Tab ist einfacher und intuitiver als je zuvor. Schau dich um!" - }, "accountActions": { "message": "Konto-Aktionen" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Du bist nicht berechtigt, diesen Eintrag zu bearbeiten" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da zuerst mit PIN oder Passwort entsperrt werden muss." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisches Entsperren ist derzeit nicht verfügbar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da die Bitwarden Desktop-App geschlossen ist." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da es für $EMAIL$ in der Bitwarden Desktop-App nicht aktiviert ist.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisches Entsperren ist derzeit aus einem unbekannten Grund nicht verfügbar." + }, "authenticating": { "message": "Authentifizierung" }, @@ -4914,13 +4828,13 @@ "message": "Wichtiger Hinweis" }, "setupTwoStepLogin": { - "message": "Richte die zweistufige Anmeldung ein" + "message": "Zwei-Faktor-Authentifizierung einrichten" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden wird einen Code an deine Konto-E-Mail-Adresse senden, um Anmeldungen von neuen Geräten ab Februar 2025 zu überprüfen." + "message": "Ab Februar 2025 wird Bitwarden einen Code an deine Konto-E-Mail-Adresse senden, um Anmeldungen von neuen Geräten zu verifizieren." }, "newDeviceVerificationNoticeContentPage2": { - "message": "Du kannst die zweistufige Anmeldung als eine alternative Methode einrichten, um deinen Account zu schützen, oder ändere deine E-Mail-Adresse zu einer, auf die du zugreifen kannst." + "message": "Du kannst die Zwei-Faktor-Authentifizierung als eine alternative Methode einrichten, um dein Konto zu schützen, oder deine E-Mail-Adresse zu einer anderen ändern, auf die du zugreifen kannst." }, "remindMeLater": { "message": "Erinnere mich später" @@ -4941,10 +4855,10 @@ "message": "Ja, ich kann zuverlässig auf meine E-Mails zugreifen" }, "turnOnTwoStepLogin": { - "message": "Aktiviere die zweistufige Anmeldung" + "message": "Zwei-Faktor-Authentifizierung aktivieren" }, "changeAcctEmail": { - "message": "Ändere die E-Mail-Adresse des Kontos" + "message": "E-Mail-Adresse des Kontos ändern" }, "extensionWidth": { "message": "Breite der Erweiterung" diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index d6ae3c52333..16fd5094fdd 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -20,16 +20,16 @@ "message": "Δημιουργία λογαριασμού" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Νέος/α στο Bitwarden;" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Σύνδεση με κλειδί πρόσβασης" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Χρήση ενιαίας σύνδεσης" }, "welcomeBack": { - "message": "Welcome back" + "message": "Καλώς ήρθατε" }, "setAStrongPassword": { "message": "Ορίστε έναν ισχυρό κωδικό πρόσβασης" @@ -120,7 +120,7 @@ "message": "Αντιγραφή κωδικού πρόσβασης" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Αντιγραφή φράσης πρόσβασης" }, "copyNote": { "message": "Αντιγραφή σημείωσης" @@ -153,13 +153,13 @@ "message": "Αντιγραφή αριθμού άδειας" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Αντιγραφή ιδιωτικού κλειδιού" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Αντιγραφή δημόσιου κλειδιού" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Αντιγραφή δακτυλικού αποτυπώματος" }, "copyCustomField": { "message": "Αντιγραφή του «$FIELD$»", @@ -177,7 +177,7 @@ "message": "Αντιγραφή σημειώσεων" }, "fill": { - "message": "Fill", + "message": "Συμπλήρωση", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "Αυτόματη συμπλήρωση ταυτότητας" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Συμπλήρωση κωδικού επαλήθευσης" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Συμπλήρωση Κωδικού Επαλήθευσης", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -443,7 +443,7 @@ "message": "Δημιουργία κωδικού πρόσβασης" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Δημιουργία φράσης πρόσβασης" }, "regeneratePassword": { "message": "Επαναδημιουργία κωδικού πρόσβασης" @@ -454,9 +454,6 @@ "length": { "message": "Μήκος" }, - "passwordMinLength": { - "message": "Ελάχιστο μήκος κωδικού πρόσβασης" - }, "uppercase": { "message": "Κεφαλαία (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Ελάχιστοι ειδικοί χαρακτήρες" }, - "avoidAmbChar": { - "message": "Αποφυγή αμφιλεγόμενων χαρακτήρων", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Αποφυγή αμφιλεγόμενων χαρακτήρων", "description": "Label for the avoid ambiguous characters checkbox." @@ -607,7 +600,7 @@ "message": "Εκκίνηση ιστοσελίδας" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Εκκίνηση ιστοσελίδας $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -859,7 +852,7 @@ "message": "Σύνδεση" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Σύνδεση στο Bitwarden" }, "restartRegistration": { "message": "Επανεκκίνηση εγγραφής" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Λίστα στοιχείων ταυτότητας στη σελίδα Καρτέλας για εύκολη αυτόματη συμπλήρωση." }, + "clickToAutofillOnVault": { + "message": "Κάντε κλικ στα αντικείμενα για αυτόματη συμπλήρωση στην προβολή Θησαυ/κίου" + }, "clearClipboard": { "message": "Εκκαθάριση Πρόχειρου", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1130,7 +1126,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Προειδοποίηση", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Μετακίνηση σε οργανισμό" }, - "share": { - "message": "Κοινοποίηση" - }, "movedItemToOrg": { "message": "$ITEMNAME$ μετακινήθηκε στο $ORGNAME$", "placeholders": { @@ -1324,10 +1317,10 @@ "message": "Εισάγετε τον 6ψήφιο κωδικό από την εφαρμογή επαλήθευσης." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Χρονικό όριο επαλήθευσης" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Λήξη χρονικού ορίου συνεδρίας επαλήθευσης. Παρακαλώ επανεκκινήστε τη διαδικασία σύνδεσης." }, "enterVerificationCodeEmail": { "message": "Εισάγετε τον 6ψήφιο κωδικό επαλήθευσης τον οποίο λάβατε στο $EMAIL$.", @@ -1447,7 +1440,7 @@ "message": "URL Διακομιστή" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "Διεύθυνση URL διακομιστή αυτοεξυπηρετητή", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1775,7 +1768,7 @@ "message": "Ταυτότητα" }, "typeSshKey": { - "message": "SSH key" + "message": "Κλειδί SSH" }, "newItemHeader": { "message": "Νέα $TYPE$", @@ -1808,13 +1801,13 @@ "message": "Ιστορικό κωδικού πρόσβασης" }, "generatorHistory": { - "message": "Generator history" + "message": "Ιστορικό γεννήτριας" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Εκκαθάριση ιστορικού γεννήτριας" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Αν συνεχίσετε, όλες οι καταχωρήσεις θα διαγραφούν οριστικά από το ιστορικό της γεννήτριας. Είστε σίγουροι ότι θέλετε να συνεχίσετε;" }, "back": { "message": "Πίσω" @@ -1853,7 +1846,7 @@ "message": "Ασφαλείς σημειώσεις" }, "sshKeys": { - "message": "SSH Keys" + "message": "Κλειδιά SSH" }, "clear": { "message": "Εκκαθάριση", @@ -1936,10 +1929,10 @@ "message": "Διαγραφή ιστορικού" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Τίποτα για προβολή" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Δεν έχετε δημιουργήσει τίποτα πρόσφατα" }, "remove": { "message": "Αφαίρεση" @@ -2047,9 +2040,6 @@ "clone": { "message": "Κλώνος" }, - "passwordGeneratorPolicyInEffect": { - "message": "Μία ή περισσότερες πολιτικές του οργανισμού επηρεάζουν τις ρυθμίσεις της γεννήτριας." - }, "passwordGenerator": { "message": "Γεννήτρια κωδικού πρόσβασης" }, @@ -2334,6 +2324,9 @@ "message": "Τομείς", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Αποκλεισμένοι τομείς" + }, "excludedDomains": { "message": "Εξαιρούμενοι Τομείς" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Το Bitwarden δε θα ρωτήσει για να αποθηκεύσετε τα στοιχεία σύνδεσης για αυτούς τους τομείς, για όλους τους συνδεδεμένους λογαριασμούς. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." }, + "blockedDomainsDesc": { + "message": "Η αυτόματη συμπλήρωση και άλλες σχετικές λειτουργίες δεν θα προσφερθούν για αυτούς τους ιστότοπους. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Ιστοσελίδα $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Οι αλλαγές αποκλεισμένων τομέων αποθηκεύτηκαν" }, @@ -2389,14 +2394,6 @@ "message": "Στοιχεία Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Αναζήτηση Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Προσθήκη Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Κείμενο" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Απόκρυψη κειμένου από προεπιλογή" }, - "maxAccessCountReached": { - "message": "Φτάσατε στον μέγιστο αριθμό πρόσβασης", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Έληξε" }, - "pendingDeletion": { - "message": "Εκκρεμεί διαγραφή" - }, "passwordProtected": { "message": "Προστατευμένο με κωδικό" }, @@ -2472,24 +2462,9 @@ "message": "Επεξεργασία Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Τι είδους Send είναι αυτό;", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Ένα φιλικό όνομα για την περιγραφή αυτού του Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Το αρχείο που θέλετε να στείλετε." - }, "deletionDate": { "message": "Ημερομηνία διαγραφής" }, - "deletionDateDesc": { - "message": "Το Send θα διαγραφεί οριστικά την καθορισμένη ημερομηνία και ώρα.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Το Send θα διαγραφεί οριστικά σε αυτήν την ημερομηνία.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Ημερομηνία λήξης" }, - "expirationDateDesc": { - "message": "Εάν οριστεί, η πρόσβαση σε αυτό το Send θα λήξει την καθορισμένη ημερομηνία και ώρα.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 ημέρα" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Προσαρμοσμένο" }, - "maximumAccessCount": { - "message": "Μέγιστος Αριθμός Πρόσβασης" - }, - "maximumAccessCountDesc": { - "message": "Εάν οριστεί, οι χρήστες δεν θα μπορούν πλέον να έχουν πρόσβαση σε αυτό το send μόλις επιτευχθεί ο μέγιστος αριθμός πρόσβασης.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Προαιρετικά απαιτείται κωδικός πρόσβασης για τους χρήστες για να έχουν πρόσβαση σε αυτό το Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Προσθέστε έναν προαιρετικό κωδικό πρόσβασης για τους παραλήπτες για πρόσβαση σε αυτήν την αποστολή.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Ιδιωτικές σημειώσεις σχετικά με αυτό το Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Απενεργοποιήστε αυτό το Send έτσι ώστε κανείς να μην μπορεί να έχει πρόσβαση σε αυτό.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Αντιγραφή του συνδέσμου για αυτό το Send στο πρόχειρο κατά την αποθήκευση.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Το κείμενο που θέλετε να στείλετε." - }, - "sendHideText": { - "message": "Απόκρυψη του κειμένου αυτού του Send από προεπιλογή.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Τρέχων αριθμός πρόσβασης" - }, "createSend": { "message": "Νέο Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Πριν ξεκινήσετε" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Για να χρησιμοποιήσετε έναν επιλογέα ημερομηνίας στυλ ημερολογίου", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "κάντε κλικ εδώ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "για να βγεις από το παράθυρο.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Η ημερομηνία λήξης που δόθηκε δεν είναι έγκυρη." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Παρουσιάστηκε σφάλμα κατά την αποθήκευση των ημερομηνιών διαγραφής και λήξης." }, - "hideEmail": { - "message": "Απόκρυψη της διεύθυνσης email μου από τους παραλήπτες." - }, "hideYourEmail": { "message": "Απόκρυψη της διεύθυνσης email σας από τους θεατές." }, - "sendOptionsPolicyInEffect": { - "message": "Μία ή περισσότερες οργανωτικές πολιτικές επηρεάζουν τις επιλογές send σας." - }, "passwordPrompt": { "message": "Προτροπή νέου κωδικού πρόσβασης" }, @@ -2884,17 +2804,28 @@ "error": { "message": "Σφάλμα" }, - "regenerateUsername": { - "message": "Επαναδημιουργία ονόματος χρήστη" + "decryptionError": { + "message": "Σφάλμα αποκρυπτογράφησης" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Το Bitwarden δεν μπόρεσε να αποκρυπτογραφήσει τα αντικείμενα θησαυ/κίου που αναφέρονται παρακάτω." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Δημιουργία ονόματος χρήστη" }, "generateEmail": { - "message": "Generate email" + "message": "Δημιουργία διεύθυνσης ηλ. ταχυδρομείου" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Η τιμή πρέπει να είναι μεταξύ $MIN$ και $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2908,7 +2839,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Χρησιμοποιήστε $RECOMMENDED$ ή περισσότερους χαρακτήρες για να δημιουργήσετε έναν ισχυρό κωδικό πρόσβασης.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2918,7 +2849,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Χρησιμοποιήστε $RECOMMENDED$ ή περισσότερες λέξεις για να δημιουργήσετε μια ισχυρή φράση πρόσβασης.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Τύπος ονόματος χρήστη" - }, "plusAddressedEmail": { "message": "Συν διεύθυνση ηλ. ταχυδρομείου", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Όνομα ιστοσελίδας" }, - "whatWouldYouLikeToGenerate": { - "message": "Τι θα θέλατε να δημιουργήσετε?" - }, - "passwordType": { - "message": "Τύπος κωδικού πρόσβασης" - }, "service": { "message": "Υπηρεσία" }, @@ -2968,11 +2890,11 @@ "message": "Δημιουργήστε ένα alias email με μια εξωτερική υπηρεσία προώθησης." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Τομέας ηλ. ταχυδρομείου", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Επιλέξτε ένα τομέα που υποστηρίζεται από την επιλεγμένη υπηρεσία", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -3175,25 +3097,25 @@ "message": "Επαναποστολή ειδοποίησης" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Δείτε όλες τις επιλογές σύνδεσης" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Δείτε όλες τις επιλογές σύνδεσης" }, "notificationSentDevice": { "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Μια ειδοποίηση στάλθηκε στη συσκευή σας" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Βεβαιωθείτε ότι ο λογαριασμός σας είναι ξεκλείδωτος και ότι η φράση δακτυλικού αποτυπώματος ταιριάζει στην άλλη συσκευή" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Θα ειδοποιηθείτε μόλις εγκριθεί η αίτηση" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Χρειάζεστε μια άλλη επιλογή;" }, "loginInitiated": { "message": "Η σύνδεση ξεκίνησε" @@ -3289,16 +3211,16 @@ "message": "Ανοίγει σε νέο παράθυρο" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Απομνημόνευση αυτής της συσκευής για την αυτόματες συνδέσεις στο μέλλον" }, "deviceApprovalRequired": { "message": "Απαιτείται έγκριση συσκευής. Επιλέξτε μια επιλογή έγκρισης παρακάτω:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Απαιτείται έγκριση συσκευής" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Επιλέξτε μια επιλογή έγκρισης παρακάτω" }, "rememberThisDevice": { "message": "Απομνημόνευση αυτής της συσκευής" @@ -3374,7 +3296,7 @@ "message": "Η διεύθυνση ηλ. ταχυδρομείου του χρήστη λείπει" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Το ηλ. ταχυδρομείο χρήστη δεν βρέθηκε. Αποσυνδεθήκατε." }, "deviceTrusted": { "message": "Αξιόπιστη συσκευή" @@ -3503,38 +3425,6 @@ "message": "Εναλλαγή σύμπτυξης", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Εισαγωγή των δεδομένων σας στο Bitwarden;", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Προστατέψτε τα δεδομένα LastPass και εισαγάγετε στο Bitwarden;", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Αποθήκευση ως μη κρυπτογραφημένο αρχείο", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Εισαγωγή στο Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Εισαγωγή...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Επιτυχής εισαγωγή δεδομένων!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Σφάλμα εισαγωγής. Ελέγξτε την κονσόλα για λεπτομέρειες.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Εμφανίστηκε σφάλμα δικτύου κατά την εισαγωγή.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Ψευδώνυμο τομέα" }, @@ -3585,11 +3475,11 @@ "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "Κωδικός Επαλήθευσης Κωδικού Πρόσβασης μιας φοράς", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "Χρόνος που απομένει πριν λήξει του τρέχον ΚΕΚΠ", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -3821,7 +3711,7 @@ "message": "Πρόσβαση" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Έχετε συνδεθεί!" }, "passkeyNotCopied": { "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί" @@ -4086,7 +3976,10 @@ "message": "Το κλειδί πρόσβασης αφαιρέθηκε" }, "autofillSuggestions": { - "message": "Προτάσεις αυτόματης συμπλήρωσης" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Αποθηκεύστε ένα αντικείμενο σύνδεσης για την αυτόματη συμπλήρωση αυτού του ιστοτόπου" @@ -4315,13 +4208,13 @@ "message": "Φίλτρα" }, "filterVault": { - "message": "Filter vault" + "message": "Φίλτρο θησαυ/κιου" }, "filterApplied": { - "message": "One filter applied" + "message": "Ένα φίλτρο εφαρμόστηκε" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ φίλτρα εφαρμόστηκαν", "placeholders": { "count": { "content": "$1", @@ -4653,23 +4546,17 @@ "message": "Τοποθεσία Αντικειμένου" }, "fileSend": { - "message": "File Send" + "message": "Δημιουργία Αποστολής" }, "fileSends": { "message": "Send αρχείων" }, "textSend": { - "message": "Text Send" + "message": "Κείμενο Αποστολής" }, "textSends": { "message": "Send κειμένων" }, - "bitwardenNewLook": { - "message": "Το Bitwarden έχει μια νέα εμφάνιση!" - }, - "bitwardenNewLookDesc": { - "message": "Είναι πιο ευκολότερο και πιο διαισθητικό από ποτέ στην αυτόματη συμπλήρωση και αναζήτηση από την καρτέλα Θησαυ/κιο. Ρίξτε μια ματιά τριγύρω!" - }, "accountActions": { "message": "Ενέργειες λογαριασμού" }, @@ -4677,7 +4564,7 @@ "message": "Εμφάνιση αριθμού προτάσεων αυτόματης συμπλήρωσης σύνδεσης στο εικονίδιο επέκτασης" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Εμφάνιση ενεργειών γρήγορης αντιγραφής στο Θησαυ/κιο" }, "systemDefault": { "message": "Προεπιλογή συστήματος" @@ -4686,16 +4573,16 @@ "message": "Οι απαιτήσεις της πολιτικής για επιχειρήσεις έχουν εφαρμοστεί σε αυτήν τη ρύθμιση" }, "sshPrivateKey": { - "message": "Private key" + "message": "Ιδιωτικό κλειδί" }, "sshPublicKey": { - "message": "Public key" + "message": "Δημόσιο κλειδί" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Δακτυλικό αποτύπωμα" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Τύπος κλειδιού" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4748,185 +4635,212 @@ "noEditPermissions": { "message": "Δεν έχετε δικαίωμα να επεξεργαστείτε αυτό το αντικείμενο" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή απαιτείται πρώτα το ξεκλείδωμα με PIN ή κωδικό πρόσβασης." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο προς το παρόν." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή η εφαρμογή Bitwarden επιφάνειας εργασίας είναι κλειστή." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Ταυτοποίηση" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "Συμπληρώση του δημιουργημένου κωδικού πρόσβασης", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Ο κωδικός επαναδημιουργήθηκε", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Αποθήκευση σύνδεσης στο Bitwarden;", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Κενό", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "Περισπωμένη", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Ανάποδη απόστροφος", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Θαυμαστικό", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Παπάκι", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "Δίεση", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Δολάριο", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Τοις εκατό", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "Εκθέτης", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Και", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Πολλαπλασιασμός", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Αριστερή παρένθεση", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Δεξιά παρένθεση", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Κάτω παύλα", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Παύλα", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Συν", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "Ίσον", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Αριστερό άγκιστρο", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Δεξί άγκιστρο", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Αριστερή αγκύλη", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Δεξιά αγκύλη", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "Κάθετος γραμμή", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Πίσω κάθετος", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Άνω κάτω τελεία", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Ερωτηματικό", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Διπλά εισαγωγικά", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Μονά εισαγωγικά", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Λιγότερο από", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Μεγαλύτερο από", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Κόμμα", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Τελεία", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Αγγλικό ερωτηματικό", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Εμπρός κάθετος", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Πεζά" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Κεφαλαία" }, "generatedPassword": { - "message": "Generated password" + "message": "Τυχαίος κωδικός πρόσβασης" }, "compactMode": { - "message": "Compact mode" + "message": "Συμπαγής μορφή" }, "beta": { - "message": "Beta" + "message": "Beta (Δοκιμαστική)" }, "importantNotice": { - "message": "Important notice" + "message": "Σημαντική ειδοποίηση" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Ρύθμιση σύνδεσης δύο βημάτων" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Το Bitwarden θα στείλει έναν κωδικό στο ηλ. ταχυδρομείο του λογαριασμού σας για να επαληθεύσει τις συνδέσεις από τις νέες συσκευές που ξεκινούν τον Φεβρουάριο του 2025." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Μπορείτε να ορίσετε σύνδεση δύο βημάτων ως εναλλακτικό τρόπο προστασίας του λογαριασμού σας ή να αλλάξετε το ηλ. ταχυδρομείο σας σε ένα που μπορείτε να έχετε πρόσβαση." }, "remindMeLater": { - "message": "Remind me later" + "message": "Υπενθύμιση αργότερα" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Έχετε αξιόπιστη πρόσβαση στο ηλ. ταχυδρομείο σας, $EMAIL$;", "placeholders": { "email": { "content": "$1", @@ -4935,24 +4849,24 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Όχι, δεν έχω" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Ναι, μπορώ να συνδεθώ αξιόπιστα στο ηλ. ταχυδρομείο μου" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Ενεργοποίηση σύνδεσης δύο βημάτων" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Αλλαγή ηλ. ταχυδρομείου λογαριασμού" }, "extensionWidth": { - "message": "Extension width" + "message": "Πλάτος εφαρμογής" }, "wide": { - "message": "Wide" + "message": "Φαρδύ" }, "extraWide": { - "message": "Extra wide" + "message": "Εξαιρετικά φαρδύ" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8fe283ba289..6b1764289f8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1004,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -2321,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2330,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2348,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2786,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3393,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -3978,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4554,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4638,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4844,5 +4868,11 @@ }, "extraWide": { "message": "Extra wide" + }, + "updateDesktopAppOrDisableFingerprintDialogTitle": { + "message": "Please update your desktop application" + }, + "updateDesktopAppOrDisableFingerprintDialogMessage": { + "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index a771b390b1b..4ea532f1e35 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy auto-fill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organisation" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organisation policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organisation policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4086,7 +3976,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to auto-fill" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 17c06b57c2a..38724849736 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy auto-fill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organisation" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organisation policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded Domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion Date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration Date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Disable this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current Access Count" - }, "createSend": { "message": "Create New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organisation policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate Username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate Username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username Type" - }, "plusAddressedEmail": { "message": "Plus Addressed Email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website Name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password Type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4086,7 +3976,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to auto-fill" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index eaff576d94b..2f87902cedb 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Longitud" }, - "passwordMinLength": { - "message": "Longitud mínima de contraseña" - }, "uppercase": { "message": "Mayúsculas (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Mínimo de caracteres especiales" }, - "avoidAmbChar": { - "message": "Evitar caracteres ambiguos", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Evitar caracteres ambiguos", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Listar los elementos de identidad en la página para facilitar el auto-rellenado." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Vaciar portapapeles", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Mover a la Organización" }, - "share": { - "message": "Compartir" - }, "movedItemToOrg": { "message": "$ITEMNAME$ se desplazó a $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clonar" }, - "passwordGeneratorPolicyInEffect": { - "message": "Una o más políticas de la organización están afectando la configuración del generador" - }, "passwordGenerator": { "message": "Generador de contraseñas" }, @@ -2334,6 +2324,9 @@ "message": "Dominios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Dominios excluidos" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no pedirá que se guarden los datos de acceso para estos dominios en todas las sesiones iniciadas. Debe actualizar la página para que los cambios surtan efecto." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Buscar Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Añadir Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Texto" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Número máximo de accesos alcanzado", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Caducado" }, - "pendingDeletion": { - "message": "Borrado pendiente" - }, "passwordProtected": { "message": "Protegido por contraseña" }, @@ -2472,24 +2462,9 @@ "message": "Editar Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "¿Qué tipo de Send es este?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Un nombre amigable para describir este Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "El archivo que desea enviar." - }, "deletionDate": { "message": "Fecha de eliminación" }, - "deletionDateDesc": { - "message": "El Send se eliminará permanentemente en la fecha y hora especificadas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Fecha de caducidad" }, - "expirationDateDesc": { - "message": "Si se establece, el acceso a este Send caducará en la fecha y hora especificadas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 día" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Personalizado" }, - "maximumAccessCount": { - "message": "Número máximo de accesos" - }, - "maximumAccessCountDesc": { - "message": "Si se establece, los usuarios ya no podrán acceder a este Send una vez que se alcance el número máximo de accesos.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Opcionalmente se requiere una contraseña para que los usuarios accedan a este Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this 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.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Desactiva este Send para que nadie pueda acceder a él.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copiar el enlace del Send en el portapapeles al guardar.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "El texto que quieres enviar." - }, - "sendHideText": { - "message": "Ocultar el texto de este Envío por defecto.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Número de acceso actual" - }, "createSend": { "message": "Crear Envío nuevo", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Antes de empezar" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Para usar un selector de fechas de estilo calendario", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "haz click aquí", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "para abrir la ventana.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "La fecha de caducidad proporcionada no es válida." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Hubo un error al guardar las fechas de eliminación y caducidad." }, - "hideEmail": { - "message": "Ocultar mi dirección de correo electrónico a los destinatarios." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Una o más políticas de organización están afectando sus opciones del Send." - }, "passwordPrompt": { "message": "Volver a preguntar contraseña maestra" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerar nombre de usuario" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generar nombre de usuario" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Tipo de nombre de usuario" - }, "plusAddressedEmail": { "message": "Dirección con sufijo", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nombre del sitio web" }, - "whatWouldYouLikeToGenerate": { - "message": "¿Qué te gustaría generar?" - }, - "passwordType": { - "message": "Tipo de contraseña" - }, "service": { "message": "Servicio" }, @@ -3503,38 +3425,6 @@ "message": "Colapsar/Expandir", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "¿Quiere importar sus datos a Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "¿Quiere proteger sus datos de LastPass e importarlos a Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guardar como archivo no cifrado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar a Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Se importaron los datos correctamente.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Se produjo un error al importar. Revise la consola para obtener detalles.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Se produjo un error de red durante la importación.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Seudónimo del dominio" }, @@ -4086,7 +3976,10 @@ "message": "Clave de acceso eliminada" }, "autofillSuggestions": { - "message": "Autocompletar sugerencias" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Guarda un elemento de inicio de sesión para este sitio para autocompletar" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden tiene un aspecto nuevo." - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Acciones de cuenta" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "No tiene permiso de editar este elemento" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index c772a7b23e7..5ee8566dc33 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Pikkus" }, - "passwordMinLength": { - "message": "Lühim lubatud parooli pikkus" - }, "uppercase": { "message": "Suurtäht (A-Z) ", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Vähim arv spetsiaalmärke" }, - "avoidAmbChar": { - "message": "Väldi ebamääraseid kirjamärke", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Väldi raskesti eristatavaid tähti ja sümboleid", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Kuvab \"Kaart\" vaates identiteete, et neid saaks kiiresti sisestada" }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Lõikelaua sisu kustutamine", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Teisalda organisatsiooni" }, - "share": { - "message": "Jaga" - }, "movedItemToOrg": { "message": "$ITEMNAME$ teisaldati $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Kloon" }, - "passwordGeneratorPolicyInEffect": { - "message": "Organisatsiooni seaded mõjutavad parooli genereerija sätteid." - }, "passwordGenerator": { "message": "Parooli genereerija" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Väljajäetud domeenid" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Otsi Sende", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Lisa Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekst" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Maksimaalne ligipääsude arv on saavutatud", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Aegunud" }, - "pendingDeletion": { - "message": "Kustutamise ootel" - }, "passwordProtected": { "message": "Parooliga kaitstud" }, @@ -2472,24 +2462,9 @@ "message": "Muuda Sendi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Mis tüüpi Send see on?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Sisesta Sendi nimi (kohustuslik).", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Fail, mida soovid saata." - }, "deletionDate": { "message": "Kustutamise kuupäev" }, - "deletionDateDesc": { - "message": "Send kustutatakse määratud kuupäeval ja kellaajal jäädavalt.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Aegumiskuupäev" }, - "expirationDateDesc": { - "message": "Selle valimisel ei pääse sellele Sendile enam pärast määratud kuupäeva ligi.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 päev" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Kohandatud" }, - "maximumAccessCount": { - "message": "Maksimaalne ligipääsude arv" - }, - "maximumAccessCountDesc": { - "message": "Selle valimisel ei saa kasutajad pärast maksimaalse ligipääsude arvu saavutamist sellele Sendile enam ligi.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Soovi korral nõua parooli, millega Sendile ligi pääseb.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Privaatne märkus selle Sendi kohta.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Keela see Send, et keegi ei pääseks sellele ligi.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopeeri Sendi salvestamisel link lõikelauale.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Tekst, mida soovid saata." - }, - "sendHideText": { - "message": "Vaikeolekus peida selle Sendi tekst.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Hetkeline ligipääsude arv" - }, "createSend": { "message": "Loo uus Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Enne alustamist" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Kalendri stiilis kuupäeva valimiseks", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "kliki siia,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "et avada Bitwarden uues aknas.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Valitud aegumiskuupäev ei ole õige." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Kustutamis- ja aegumiskuupäevade salvestamisel ilmnes tõrge." }, - "hideEmail": { - "message": "Ära näita saajatele minu e-posti aadressi." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Organisatsiooni seaded mõjutavad sinu Sendi sätteid." - }, "passwordPrompt": { "message": "Nõutav on ülemparool" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Viga" }, - "regenerateUsername": { - "message": "Genereeri kasutajanimi uuesti" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Genereeri kasutajanimi" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Kasutajanime tüüp" - }, "plusAddressedEmail": { "message": "Plussiga e-posti aadress", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Veebilehe nimi" }, - "whatWouldYouLikeToGenerate": { - "message": "Mida sa soovid genereerida?" - }, - "passwordType": { - "message": "Parooli tüüp" - }, "service": { "message": "Teenus" }, @@ -3503,38 +3425,6 @@ "message": "Peida", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Impordin andmed Bitwardenisse?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Kaitse oma LastPassi andmeid ja impordi need Bitwardenisse?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salvesta ilma krüpteeringuta failina", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Impordi Bitwardenisse", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importimine...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Andmed on edukalt imporditud!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Ilmnes viga. Vaata täpsemaid andmeid konsoolist.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Importimisel ilmnes võrgu viga.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domeen" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 117751b3794..d7bcfc2838e 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Luzera" }, - "passwordMinLength": { - "message": "Pasahitzaren gutxieneko luzera" - }, "uppercase": { "message": "Letra larria (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Gutxieneko karaktere bereziak" }, - "avoidAmbChar": { - "message": "Saihestu karaktere anbiguoak", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Erakutsi identitateak fitxa orrian, erraz auto-betetzeko." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Hustu arbela", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Mugitu erakundera" }, - "share": { - "message": "Partekatu" - }, "movedItemToOrg": { "message": "$ITEMNAME$ $ORGNAME$-ra mugituta", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Klonatu" }, - "passwordGeneratorPolicyInEffect": { - "message": "Erakundeko politika batek edo gehiagok sortzailearen konfigurazioari eragiten diote." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Kanporatutako domeinuak" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Send-ak bilatu", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Gehitu Send-a", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Testua" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Sarbide kopuru maximoa gaindituta", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Iraungita" }, - "pendingDeletion": { - "message": "Ezabatzea egiteke" - }, "passwordProtected": { "message": "Pasahitz babestua" }, @@ -2472,24 +2462,9 @@ "message": "Editatu Send-a", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Zein Send mota da hau?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Send hau deskribatzeko izena.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Bidali nahi duzun fitxategia." - }, "deletionDate": { "message": "Ezabatze data" }, - "deletionDateDesc": { - "message": "Send-a betiko ezabatuko da zehaztutako datan eta orduan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Iraungitze data" }, - "expirationDateDesc": { - "message": "Hala ezartzen bada, Send honetarako sarbidea zehaztutako egunean eta orduan amaituko da.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "Egun 1" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Pertsonalizatua" }, - "maximumAccessCount": { - "message": "Sarbide kopuru maximoa" - }, - "maximumAccessCountDesc": { - "message": "Hala ezartzen bada, erabiltzaileak ezin izango dira Send honetara sartu gehienezko sarbide kopurura iritsi ondoren.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Nahi izanez gero, pasahitza eskatu erabiltzaileak Send honetara sar daitezen.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Send honi buruzko ohar pribatuak.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Desgaitu Send hau inor sar ez dadin.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Gordetzean, kopiatu Send honen esteka arbelean.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Bidali nahi duzun testua." - }, - "sendHideText": { - "message": "Ezkutatu Send-eko testu hau, modu lehenetsian.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Uneko sarbide kopurua" - }, "createSend": { "message": "Sortu Send berria", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Hasi aurretik" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Data aukeratzeko egutegi modua erabiltzeko", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klikatu hemen", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "leihoa irekitzeko.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Iraungitze data ez da baliozkoa." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Akatsa gertatu da ezabatze eta iraungitze datak gordetzean." }, - "hideEmail": { - "message": "Ezkutatu nire emaila hartzaileei." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Erakundeko politika batek edo gehiagok Send-eko aukerei eragiten diote." - }, "passwordPrompt": { "message": "Berriro eskatu pasahitz nagusia" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Akatsa" }, - "regenerateUsername": { - "message": "Berrezarri erabiltzaile izena" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Sortu erabiltzaile izena" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Erabiltzaile izen mota" - }, "plusAddressedEmail": { "message": "Atzizkidun emaila", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Webgune izena" }, - "whatWouldYouLikeToGenerate": { - "message": "Zer sortu nahi duzu?" - }, - "passwordType": { - "message": "Pasahitz mota" - }, "service": { "message": "Zerbitzua" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Inportatzen...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Datuak zuzen inportatu dira!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Errorea gertatu da inportatzean. Begiratu xehetasunak kontsolan.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Sareko errorea gertatu da inportatzerakoan.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 47bf414db2f..3cb1581b98f 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -454,9 +454,6 @@ "length": { "message": "طول" }, - "passwordMinLength": { - "message": "حداقل طول گذرواژه" - }, "uppercase": { "message": "حروف بزرگ (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "حداقل حرف خاص" }, - "avoidAmbChar": { - "message": "از کاراکترهای مبهم اجتناب کن", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "موارد هویتی را در صفحه برگه برای پر کردن خودکار آسان فهرست کن." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "پاکسازی کلیپ بورد", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "انتقال به سازمان" }, - "share": { - "message": "اشتراک گذاری" - }, "movedItemToOrg": { "message": "$ITEMNAME$ منتقل شد به $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "شبیه سازی" }, - "passwordGeneratorPolicyInEffect": { - "message": "یک یا چند سیاست سازمان بر تنظیمات تولید کننده شما تأثیر می‌گذارد." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "دامنه های مستثنی" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "جستجوی ارسال‌ها", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "افزودن ارسال", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "متن" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "به حداکثر تعداد دسترسی رسیده است", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "منقضی شده" }, - "pendingDeletion": { - "message": "در انتظار حذف" - }, "passwordProtected": { "message": "محافظت ‌شده با کلمه عبور" }, @@ -2472,24 +2462,9 @@ "message": "ویرایش ارسال", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "این چه نوع ارسالی است؟", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "یک نام دوستانه برای توصیف این ارسال.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "پرونده ای که می‌خواهید ارسال کنید." - }, "deletionDate": { "message": "تاریخ حذف" }, - "deletionDateDesc": { - "message": "ارسال در تاریخ و ساعت مشخص شده برای همیشه حذف خواهد شد.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "تاريخ انقضاء" }, - "expirationDateDesc": { - "message": "در صورت تنظیم، دسترسی به این ارسال در تاریخ و ساعت مشخص شده منقضی می‌شود.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "۱ روز" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "سفارشی" }, - "maximumAccessCount": { - "message": "تعداد دسترسی حداکثر" - }, - "maximumAccessCountDesc": { - "message": "در صورت تنظیم، با رسیدن به حداکثر تعداد دسترسی، کاربران دیگر نمی‌توانند به این ارسال دسترسی پیدا کنند.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "به صورت اختیاری برای دسترسی کاربران به این ارسال به یک کلمه عبور نیاز دارید.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "یادداشت های خصوصی در مورد این ارسال.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "این ارسال را غیرفعال کنید تا کسی نتواند به آن دسترسی پیدا کند.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "پس از ذخیره، این پیوند ارسال را به کلیپ بورد کپی کن.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "متنی که می‌خواهید ارسال کنید." - }, - "sendHideText": { - "message": "متن این ارسال را به طور پیش فرض پنهان کن.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "تعداد دسترسی فعلی" - }, "createSend": { "message": "ارسال جدید", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "قبل از اینکه شروع کنی" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "برای استفاده از انتخابگر تاریخ به سبک تقویم", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "اینجا کلیک کنید", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "برای خروج از پنجره خود.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "تاریخ انقضاء ارائه شده معتبر نیست." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "هنگام ذخیره حذف و تاریخ انقضاء شما خطایی روی داد." }, - "hideEmail": { - "message": "نشانی ایمیلم را از گیرندگان مخفی کن." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "یک یا چند سیاست سازمان بر گزینه های ارسال شما تأثیر می‌گذارد." - }, "passwordPrompt": { "message": "درخواست مجدد کلمه عبور اصلی" }, @@ -2884,8 +2804,19 @@ "error": { "message": "خطا" }, - "regenerateUsername": { - "message": "ایجاد مجدد نام کاربری" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "ایجاد نام کاربری" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "نوع نام کاربری" - }, "plusAddressedEmail": { "message": "به علاوه نشانی ایمیل داده شده", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "نام وب‌سایت" }, - "whatWouldYouLikeToGenerate": { - "message": "چه چیزی دوست دارید تولید کنید؟" - }, - "passwordType": { - "message": "نوع کلمه عبور" - }, "service": { "message": "سرویس" }, @@ -3503,38 +3425,6 @@ "message": "دکمه بستن", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "دامنه مستعار" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index f0104488d32..9e2f18ea72e 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -193,10 +193,10 @@ "message": "Automaattitäytä henkilöllisyys" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Täytä vahvistuskoodi" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Täytä vahvistuskoodi", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -454,9 +454,6 @@ "length": { "message": "Pituus" }, - "passwordMinLength": { - "message": "Salasanan vähimmäispituus" - }, "uppercase": { "message": "Isot kirjaimet (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Erikoismerkkejä vähintään" }, - "avoidAmbChar": { - "message": "Vältä epäselviä merkkejä", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Vältä epäselviä merkkejä", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Näytä henkilöllisyydet Välilehti-sivulla automaattitäytön helpottamiseksi." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Tyhjennä leikepöytä", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Siirrä organisaatiolle" }, - "share": { - "message": "Jaa" - }, "movedItemToOrg": { "message": "$ITEMNAME$ siirrettiin organisaatioon $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Kloonaa" }, - "passwordGeneratorPolicyInEffect": { - "message": "Yksi tai useampi organisaatiokäytäntö vaikuttaa generaattorisi asetuksiin." - }, "passwordGenerator": { "message": "Salasanageneraattori" }, @@ -2115,7 +2105,7 @@ "message": "Aikakatkaisutoiminnon vahvistus" }, "autoFillAndSave": { - "message": "Täytä automaattisesti ja tallenna" + "message": "Automaattitäytä ja tallenna" }, "fillAndSave": { "message": "Täytä ja tallenna" @@ -2334,6 +2324,9 @@ "message": "Verkkotunnukset", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Ohitettavat verkkotunnukset" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näillä verkkotunnuksilla. Koskee kaikkia kirjautuneita tilejä. Ota muutokset käyttöön päivittämällä sivu." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Verkkotunnus $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Rajoitettujen verkkotunnusten muutokset tallennettiin" }, @@ -2389,14 +2394,6 @@ "message": "Sendin tiedot", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Etsi Sendeistä", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Lisää Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Teksti" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Piilota teksti oletuksena" }, - "maxAccessCountReached": { - "message": "Käyttökertojen enimmäismäärä saavutettu", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Erääntynyt" }, - "pendingDeletion": { - "message": "Odottaa poistoa" - }, "passwordProtected": { "message": "Salasanasuojattu" }, @@ -2472,24 +2462,9 @@ "message": "Muokkaa Sendiä", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Minkä tyyppinen Send tämä on?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Kuvaava nimi Sendille.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Tiedosto, jonka haluat lähettää." - }, "deletionDate": { "message": "Poistoajankohta" }, - "deletionDateDesc": { - "message": "Send poistuu pysyvästi määritettynä ajankohtana.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send poistuu pysyvästi tänä päivänä.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Erääntymisajankohta" }, - "expirationDateDesc": { - "message": "Jos määritetty, Send erääntyy määritettynä ajankohtana.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 päivä" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Mukautettu" }, - "maximumAccessCount": { - "message": "Käyttöoikeuksien enimmäismäärä" - }, - "maximumAccessCountDesc": { - "message": "Jos määritetty, käyttäjät eivät voi avata Sendiä käyttökertojen enimmäismäärän täytyttyä.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Halutessasi, vaadi käyttäjiä syöttämään salasana Sendin avaamiseksi.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Lisää valinnainen salasana vastaanottajille tähän Sendiin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Yksityisiä merkintöjä tästä Sendistä.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Poista Send käytöstä, jottei kukaan voi avata sitä.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopioi Sendin linkki leikepöydälle tallennettaessa.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Teksti, jonka haluat lähettää." - }, - "sendHideText": { - "message": "Piilota Sendin teksti oletuksena.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Käyttökertojen nykyinen määrä" - }, "createSend": { "message": "Uusi Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Ennen kuin aloitat" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Käyttääksesi kalenterityylistä päivämäärän valintaa", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klikkaa tästä", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "erillistä valintaa varten.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Määritetty erääntymismisajankohta on virheellinen." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Tapahtui virhe tallennettaessa poisto- ja erääntymisajankohtia." }, - "hideEmail": { - "message": "Piilota sähköpostiosoitteeni vastaanottajilta." - }, "hideYourEmail": { "message": "Piilota sähköpostiosoitteeni avaajilta." }, - "sendOptionsPolicyInEffect": { - "message": "Yksi tai useampi organisaatiokäytäntö vaikuttaa Send-asetuksiisi." - }, "passwordPrompt": { "message": "Pääsalasanan uudelleenkysely" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Virhe" }, - "regenerateUsername": { - "message": "Luo uusi käyttäjätunnus" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Luo käyttäjätunnus" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Käyttäjätunnuksen tyyppi" - }, "plusAddressedEmail": { "message": "Plus+merkkinen sähköposti", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Verkkosivuston nimi" }, - "whatWouldYouLikeToGenerate": { - "message": "Mitä haluat luoda?" - }, - "passwordType": { - "message": "Salasanan tyyppi" - }, "service": { "message": "Palvelu" }, @@ -3503,38 +3425,6 @@ "message": "Laajenna tai supista", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Haluatko tuoda tietosi Bitwardeniin?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Haluatko suojata LastPass-tietosi tuomalla ne Bitwardeniin?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Tallenna salaamattomana tiedostona", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Tuo Bitwardeniin", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Tuodaan...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Tietojen tuonti onnistui.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Tuontivirhe. Näet isätietoja hallinnasta.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Verkkovirhe tuonnin aikana.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliaksen verkkotunnus" }, @@ -3585,11 +3475,11 @@ "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "Aikaperusteinen kertakäyttöinen salasanan vahvistuskoodi", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "Aika jäljellä, ennen kuin nykyinen TOTP vanhenee", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -4086,7 +3976,10 @@ "message": "Pääsyavain poistettiin" }, "autofillSuggestions": { - "message": "Automaattitäytön ehdotukset" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Tallenna tälle sivustolle automaattisesti täytettävä kirjautumistieto." @@ -4664,12 +4557,6 @@ "textSends": { "message": "Teksti-Sendit" }, - "bitwardenNewLook": { - "message": "Bitwardenilla on uusi ulkoasu!" - }, - "bitwardenNewLookDesc": { - "message": "Automaattinen täyttö ja sisällön haku Holvi-välilehdeltä on nyt entistä helpompaa ja luontevampaa. Kokeile nyt!" - }, "accountActions": { "message": "Tilitoiminnot" }, @@ -4677,7 +4564,7 @@ "message": "Näytä automaattitäytön ehdotusten määrä laajennuksen kuvakkeessa" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Näytä pikakopiointitoiminnot holvissa" }, "systemDefault": { "message": "Järjestelmän oletus" @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Sinulla ei ole oikeutta muokata tätä kohdetta" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Todennetaan" }, @@ -4911,19 +4825,19 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Tärkeä ilmoitus" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Määritä kaksivaiheinen kirjautuminen" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen." }, "newDeviceVerificationNoticeContentPage2": { "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Muistuta myöhemmin" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -4941,10 +4855,10 @@ "message": "Yes, I can reliably access my email" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Ota kaksivaiheinen kirjautuminen käyttöön" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Muuta tilin sähköpostiosoitetta" }, "extensionWidth": { "message": "Laajennuksen leveys" diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index ebaf884a141..62e962e309a 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Kahabaan" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Inakamababang espesyal" }, - "avoidAmbChar": { - "message": "Iwasan ang mga hindi malinaw na character", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Itala ang mga item ng pagkatao sa Tab page para sa madaling auto-fill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Linisin ang clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Lumipat sa organisasyon" }, - "share": { - "message": "I-share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ lumipat sa $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Kopyahin ang item" }, - "passwordGeneratorPolicyInEffect": { - "message": "Isang o higit pang patakaran ng organisasyon ay nakakaapekto sa iyong mga setting ng generator." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Inilayo na Domain" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Maghanap ng Mga Padala", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Idagdag ang Padala", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Teksto" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Ang pinaka-access count ay nakaabot na", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Paso na" }, - "pendingDeletion": { - "message": "Nakabinbing pagbura" - }, "passwordProtected": { "message": "Protektado ng Password" }, @@ -2472,24 +2462,9 @@ "message": "I-edit ang Ipadala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Anong uri ng Ipadala ang ito?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Isang friendly name upang ilarawan ang Ipadala na ito.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Ang file na gusto mong ipadala." - }, "deletionDate": { "message": "Petsa ng Pagtanggal" }, - "deletionDateDesc": { - "message": "Ang Ipadala ay tatanggalin nang permanente sa tinukoy na petsa at oras.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Petsa ng pag-expire" }, - "expirationDateDesc": { - "message": "Kung nakatakda, ang access sa Send na ito ay mag-expire sa tinukoy na petsa at oras.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 araw" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Pasadyang" }, - "maximumAccessCount": { - "message": "Pinakamataas na Bilang ng Access" - }, - "maximumAccessCountDesc": { - "message": "Kung nakatakda, ang mga user ay hindi na maaaring ma-access ang Send na ito pagkatapos makarating sa maximum access count.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Maipapayo na mag-require ng password para sa mga user na ma-access ang Send na ito.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Pribadong mga tala tungkol sa Send na ito.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "I-deactivate ang Send na ito para hindi na ito ma-access.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopyahin ang link ng Send na ito sa clipboard pagkatapos i-save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Ang teksto na nais mong ipadala." - }, - "sendHideText": { - "message": "Itago ang default na teksto ng Send na ito.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Kasalukuyang access count" - }, "createSend": { "message": "Bagong Ipadala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Bago ka magsimula" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Upang gamitin ang isang calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "mag-click dito", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "upang bumalik sa iyong window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Ang ibinigay na petsa ng pagpaso ay hindi wasto." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Nagkaroon ng error sa pag-save ng iyong mga petsa ng pagbura at pagpaso." }, - "hideEmail": { - "message": "Itago ang aking email address mula sa mga tatanggap." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Isang o higit pang mga patakaran ng organisasyon ay nakaapekto sa iyong mga pagpipilian sa Pagpadala." - }, "passwordPrompt": { "message": "Muling pagsusuri sa master password" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Mali" }, - "regenerateUsername": { - "message": "Muling bumuo ng username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Lumikha ng username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Uri ng username" - }, "plusAddressedEmail": { "message": "Plus na naka-address na email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Pangalan ng website" }, - "whatWouldYouLikeToGenerate": { - "message": "Ano ang nais mong bumuo?" - }, - "passwordType": { - "message": "Uri ng Password" - }, "service": { "message": "Serbisyo" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 0ff35958be0..b1ebfe05fe1 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Longueur" }, - "passwordMinLength": { - "message": "Longueur minimale du mot de passe" - }, "uppercase": { "message": "Majuscules (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum de caractères spéciaux" }, - "avoidAmbChar": { - "message": "Éviter les caractères ambigus", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Éviter les caractères ambigus", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Liste les éléments d'identité sur la Page d'onglet pour faciliter la saisie automatique." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Effacer le presse-papiers", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Déplacer vers l'organisation" }, - "share": { - "message": "Partager" - }, "movedItemToOrg": { "message": "$ITEMNAME$ a été déplacé vers $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Cloner" }, - "passwordGeneratorPolicyInEffect": { - "message": "Une ou plusieurs politiques de sécurité de l'organisation affectent les paramètres de votre générateur." - }, "passwordGenerator": { "message": "Générateur de mot de passe" }, @@ -2334,6 +2324,9 @@ "message": "Domaines", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domaines bloqués" + }, "excludedDomains": { "message": "Domaines exclus" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ne demandera pas d'enregistrer les détails de connexion pour ces domaines pour tous les comptes connectés. Vous devez actualiser la page pour que les modifications prennent effet." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Site web $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Changements de domaines exclus enregistrés" }, @@ -2389,14 +2394,6 @@ "message": "Détails du Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Rechercher dans les Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Ajouter un Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Texte" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Masquer le texte par défaut" }, - "maxAccessCountReached": { - "message": "Nombre maximum d'accès atteint", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expiré" }, - "pendingDeletion": { - "message": "En attente de suppression" - }, "passwordProtected": { "message": "Protégé par un mot de passe" }, @@ -2472,24 +2462,9 @@ "message": "Modifier le Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "De quel type de Send s'agit-il ?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Un nom convivial pour décrire ce Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Le fichier que vous voulez envoyer." - }, "deletionDate": { "message": "Date de suppression" }, - "deletionDateDesc": { - "message": "Le Send sera définitivement supprimé à la date et heure spécifiées.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Le Send sera définitivement supprimé à la date spécifiée.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Date d'expiration" }, - "expirationDateDesc": { - "message": "Si défini, l'accès à ce Send expirera à la date et heure spécifiées.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 jour" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Personnalisé" }, - "maximumAccessCount": { - "message": "Nombre maximum d'accès" - }, - "maximumAccessCountDesc": { - "message": "Si défini, les utilisateurs ne seront plus en mesure d'accéder à ce Send une fois que le nombre maximum d'accès sera atteint.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Vous pouvez, si vous le souhaitez, exiger un mot de passe pour accéder à ce Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Ajouter un mot de passe facultatif pour que les destinataires puissent accéder à ce Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Notes privées à propos de ce Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Désactiver ce Send pour que personne ne puisse y accéder.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copier le lien de ce Send dans le presse-papiers lors de l'enregistrement.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Le texte que vous voulez envoyer." - }, - "sendHideText": { - "message": "Masquer le texte de ce Send par défaut.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Nombre d'accès actuel" - }, "createSend": { "message": "Nouveau Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Avant de commencer" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Pour utiliser un sélecteur de date en forme de calendrier,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "cliquez ici", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "pour ouvrir une nouvelle fenêtre.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "La date d'expiration indiquée n'est pas valide." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Une erreur s'est produite lors de l'enregistrement de vos dates de suppression et d'expiration." }, - "hideEmail": { - "message": "Masquer mon adresse électronique aux destinataires." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Une ou plusieurs politiques de sécurité de l'organisation affectent vos options Send." - }, "passwordPrompt": { "message": "Ressaisir le mot de passe principal" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Erreur" }, - "regenerateUsername": { - "message": "Régénérer un nom d'utilisateur" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Générer un nom d'utilisateur" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Type de nom d'utilisateur" - }, "plusAddressedEmail": { "message": "Courriel sous-adressé", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nom du site web" }, - "whatWouldYouLikeToGenerate": { - "message": "Que souhaitez-vous générer ?" - }, - "passwordType": { - "message": "Type de mot de passe" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Déplier/Replier", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importer vos données dans Bitwarden ?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protéger vos données LastPass et importer dans Bitwarden ?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Enregistrer en tant que fichier non chiffré", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importer vers Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importation en cours...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Données importées avec succès !", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erreur lors de l'importation. Consultez la console pour plus de détails.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Une erreur réseau s'est produite lors de l'importation.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domaine de l'alias" }, @@ -4086,7 +3976,10 @@ "message": "Clé d'identification (passkey) retirée" }, "autofillSuggestions": { - "message": "Suggestions de saisie automatique" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Enregistrez un élément de connexion à remplir automatiquement pour ce site" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden a un nouveau look !" - }, - "bitwardenNewLookDesc": { - "message": "Il est plus facile et plus intuitif que jamais de remplir automatiquement les champs et d'effectuer des recherches à partir de l'onglet \"Coffre\". Jetez un coup d'œil !" - }, "accountActions": { "message": "Actions du compte" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Vous n'êtes pas autorisé à modifier cet élément" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authentification" }, @@ -4911,22 +4825,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Avis important" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Configurer l'identification à deux facteurs" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden enverra un code au courriel de votre compte pour vérifier les connexions depuis de nouveaux appareils à partir de février 2025." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Vous pouvez configurer l'identification à deux facteurs comme un moyen alternatif de protéger votre compte ou de changer votre adresse courriel à une autre à laquelle vous pouvez accéder." }, "remindMeLater": { - "message": "Remind me later" + "message": "Me le rappeler plus tard" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Avez-vous un accès fiable à votre courriel $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4935,16 +4849,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Non, je ne l'ai pas" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Oui, je peux accéder à mon courriel de manière fiable" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Activer l'identification à deux facteurs" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Changer le courriel du compte" }, "extensionWidth": { "message": "Largeur de l'extension" diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index beeae76232a..3d352359e69 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3,39 +3,39 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Xestor de contrasinais", + "message": "Xestor de Contrasinais Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Na casa, no traballo ou en ruta, Bitwarden protexe os teus contrasinais, chaves de acceso e datos sensíbeis", + "message": "Na casa, no traballo ou en movemento, Bitwarden protexe os teus contrasinais, Claves de acceso e datos sensíbeis", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { - "message": "Rexístrate ou crea unha nova conta para acceder á túa caixa forte." + "message": "Inicia sesión ou rexístrate para acceder á túa caixa forte." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Invitación aceptada" }, "createAccount": { "message": "Crea unha conta" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Novo en Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Iniciar sesión con Clave de acceso" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usar inicio de sesión único" }, "welcomeBack": { - "message": "Welcome back" + "message": "Benvido de novo" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Crea un contrasinal forte" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Remata de crear a túa conta creando un contrasinal" }, "enterpriseSingleSignOn": { "message": "Inicio de sesión único empresarial" @@ -62,7 +62,7 @@ "message": "Unha pista do contrasinal mestre pode axudarte a lembrar o teu contrasinal se o esqueces." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Se esqueces o teu contrasinal, pódese enviar a pista do contrasinal ó teu correo. Máximo de $CURRENT$/$MAXIMUM$ caracteres.", "placeholders": { "current": { "content": "$1", @@ -81,10 +81,10 @@ "message": "Pista do contrasinal mestre (opcional)" }, "joinOrganization": { - "message": "Join organization" + "message": "Unirse a esta organización" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "Unirse a $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -93,10 +93,10 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "Remata de unirte á organización creando un contrasinal mestre." }, "tab": { - "message": "Separador" + "message": "Pestana" }, "vault": { "message": "Caixa forte" @@ -114,13 +114,13 @@ "message": "Axustes" }, "currentTab": { - "message": "Separador actual" + "message": "Pestana actual" }, "copyPassword": { "message": "Copiar contrasinal" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copiar frase de contrasinal" }, "copyNote": { "message": "Copiar nota" @@ -138,31 +138,31 @@ "message": "Copiar código de seguranza" }, "copyName": { - "message": "Copy name" + "message": "Copiar nome" }, "copyCompany": { - "message": "Copy company" + "message": "Copiar empresa" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Copiar número de Seguridade Social" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Copiar número de pasaporte" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Copiar número de matrícula" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Copiar clave privada" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Copiar clave pública" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Copiar pegada dixital" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Copiar $FIELD$", "placeholders": { "field": { "content": "$1", @@ -171,32 +171,32 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copiar sitio web" }, "copyNotes": { - "message": "Copy notes" + "message": "Copiar notas" }, "fill": { - "message": "Fill", + "message": "Encher", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { - "message": "Auto-encher" + "message": "Autoencher" }, "autoFillLogin": { - "message": "Encher automaticamente inicio de sesión" + "message": "Autoencher credenciais" }, "autoFillCard": { - "message": "Encher automaticamente tarxeta" + "message": "Autoencher tarxeta" }, "autoFillIdentity": { - "message": "Encher automaticamente identidade" + "message": "Autoencher automaticamente identidade" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Encher código de verificación" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Encher código de verificación", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -206,7 +206,7 @@ "message": "Copiar nome de campo personalizado" }, "noMatchingLogins": { - "message": "Sen inicios de sesión coincidentes" + "message": "Sen credenciais coincidentes" }, "noCards": { "message": "Sen tarxetas" @@ -215,7 +215,7 @@ "message": "Sen identidades" }, "addLoginMenu": { - "message": "Engadir inicio de sesión" + "message": "Engadir credenciais" }, "addCardMenu": { "message": "Engadir tarxeta" @@ -224,31 +224,31 @@ "message": "Engadir identidade" }, "unlockVaultMenu": { - "message": "Desbloquear a súa caixa forte" + "message": "Desbloquear a caixa forte" }, "loginToVaultMenu": { - "message": "Rexistrarse na súa caixa forte" + "message": "Iniciar sesión na caixa forte" }, "autoFillInfo": { - "message": "Non hai inicios de sesión dispoñíbeis para encher automaticamente para o separador actual do navegador." + "message": "Non hai credenciais dispoñibles para autoenchido na pestana actual." }, "addLogin": { - "message": "Engadir inicio de sesión" + "message": "Engadir unha credencial" }, "addItem": { - "message": "Engadir elemento" + "message": "Engadir entrada" }, "accountEmail": { - "message": "Account email" + "message": "Correo electrónico da conta" }, "requestHint": { - "message": "Request hint" + "message": "Solicitar pista" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Solicitar pista do contrasinal" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Introduce o teu correo electrónico e enviarémosche a túa pista do contrasinal mestre" }, "passwordHint": { "message": "Pista do contrasinal" @@ -284,29 +284,29 @@ "message": "Continuar á aplicación web?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Descobre máis funcionalidades da túa conta de Bitwarden na aplicación web." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Ir ó Centro de Axuda?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Aprende máis acerca de como usar Bitwarden no Centro de Axuda." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Ir á tenda de extensións do navegador?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Axuda a outros a saber que Bitwarden é o que buscan! Visita a tenda de extensións do navegador e déixanos a túa opinión." }, "changeMasterPasswordOnWebConfirmation": { "message": "Podes cambiar o teu contrasinal mestre na aplicación web de Bitwarden." }, "fingerprintPhrase": { - "message": "Frase de pegada dactilar", + "message": "Frase de pegada dixital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { - "message": "Frase de pegada dactilar da túa conta", + "message": "Frase de pegada dixital da túa conta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "twoStepLogin": { @@ -316,43 +316,43 @@ "message": "Pechar sesión" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Acerca de Bitwarden" }, "about": { "message": "Acerca de" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Máis de Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Ir a bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden para Empresas" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Autenticador Bitwarden" }, "continueToAuthenticatorPageDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" + "message": "O Autenticador Bitwarden permite almacenar claves de autenticación e xerar códigos de un uso para fluxos de verificación en 2 pasos. Aprende máis en bitwarden.com" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Xestor de Segredos de Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Almacenar, xestionar e compartir de maneira segura segredos de desarrollo co Xestor de Segredos de Bitwarden. Aprende máis en bitwarden.com." }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "Crea experiencias de inicio de sesión cómodas e seguras libres de contrasinais tradicionais con Passwordless.dev. Descobre como en bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Plan Familiar Gratuíto Bitwarden" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "Es candidato para Bitwarden Familiar Gratuíto. Troca esta oferta hoxe mesmo na aplicación web." }, "version": { "message": "Versión" @@ -373,22 +373,22 @@ "message": "Editar cartafol" }, "newFolder": { - "message": "New folder" + "message": "Novo cartafol" }, "folderName": { - "message": "Folder name" + "message": "Nome do cartafol" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "Aniñar un cartafol engadindo o nome do cartafol pai seguido dun \"/\". Exemplo: Social/Foros" }, "noFoldersAdded": { - "message": "No folders added" + "message": "Sen cartafois" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "Crea cartafoles para organizar as entradas da túa caixa forte" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "Estás seguro de querer eliminar permanentemente este cartafol?" }, "deleteFolder": { "message": "Eliminar cartafol" @@ -428,13 +428,13 @@ "description": "Short for 'credential generator'." }, "passGenInfo": { - "message": "Xera automaticamente contrasinais fortes e únicos para os seus inicios de sesión." + "message": "Xera automaticamente contrasinais fortes e únicos para os teus inicios de sesión." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Aplicación web de Bitwarden" }, "importItems": { - "message": "Importar elementos" + "message": "Importar entradas" }, "select": { "message": "Seleccionar" @@ -443,7 +443,7 @@ "message": "Xerar contrasinal" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Xerar frase de contrasinal" }, "regeneratePassword": { "message": "Volver xerar contrasinal" @@ -454,9 +454,6 @@ "length": { "message": "Lonxitude" }, - "passwordMinLength": { - "message": "Lonxitude mínima do contrasinal" - }, "uppercase": { "message": "Maiúsculas (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -474,11 +471,11 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "Incluír", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Incluír caracteres en maiúsculas", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -486,7 +483,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Incluír caracteres en minúsculas", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -494,7 +491,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Incluír números", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -502,7 +499,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Incluír caracteres especiais", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -516,28 +513,24 @@ "message": "Separador de palabras" }, "capitalize": { - "message": "Facer a primeira letra da palabra maiúscula", + "message": "Primeira letra maiúscula", "description": "Make the first letter of a work uppercase." }, "includeNumber": { "message": "Incluír número" }, "minNumbers": { - "message": "Mínimo de números" + "message": "Mínimo de cifras" }, "minSpecial": { "message": "Mínimo de caracteres especiais" }, - "avoidAmbChar": { - "message": "Evitar caracteres ambiguos", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Evitar caracteres ambiguos", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "Os requisitos da política empresarial foron aplicados ás opcións do teu xerador.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -550,10 +543,10 @@ "message": "Ver" }, "noItemsInList": { - "message": "Non hai elementos que listar." + "message": "Non hai entradas que listar." }, "itemInformation": { - "message": "Información do elemento" + "message": "Información da entrada" }, "username": { "message": "Nome de usuario" @@ -562,7 +555,7 @@ "message": "Contrasinal" }, "totp": { - "message": "Secreto de autenticador" + "message": "Segredo de autenticador" }, "passphrase": { "message": "Frase de contrasinal" @@ -571,19 +564,19 @@ "message": "Favorito" }, "unfavorite": { - "message": "Unfavorite" + "message": "Suprimir dos favoritos" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Engadido a favoritos" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Eliminado dos favoritos" }, "notes": { "message": "Notas" }, "privateNote": { - "message": "Private note" + "message": "Nota privada" }, "note": { "message": "Nota" @@ -604,10 +597,10 @@ "message": "Iniciar" }, "launchWebsite": { - "message": "Launch website" + "message": "Abrir web" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Abrir web $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -631,16 +624,16 @@ "message": "Opcións de desbloqueo" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Configura un método de desbloqueo para cambiar a túa acción de peche de sesión da caixa forte." + "message": "Configura un método de desbloqueo para cambiar a acción do temporizador da caixa forte." }, "unlockMethodNeeded": { "message": "Configura un método de desbloqueo nos axustes" }, "sessionTimeoutHeader": { - "message": "Tempo de sesión esgotado" + "message": "Temporizador da sesión esgotado" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Temporizador da caixa forte" }, "otherOptions": { "message": "Outras opcións" @@ -649,7 +642,7 @@ "message": "Valorar a extensión" }, "browserNotSupportClipboard": { - "message": "O teu navegador web non soporta copia doada ao portapapeis. Cópiao manualmente no seu lugar." + "message": "O navegador non permite copia doada ó portapapeis. Debes copialo manualmente." }, "verifyIdentity": { "message": "Verificar identidade" @@ -658,19 +651,19 @@ "message": "A túa caixa forte está bloqueada. Verifica a túa identidade para continuar." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "A caixa forte está pechada" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "A conta está pechada" }, "or": { - "message": "or" + "message": "ou" }, "unlock": { "message": "Desbloquear" }, "loggedInAsOn": { - "message": "Conectado coma $EMAIL$ en $HOSTNAME$.", + "message": "Conectado como $EMAIL$ en $HOSTNAME$.", "placeholders": { "email": { "content": "$1", @@ -689,13 +682,13 @@ "message": "Tempo de espera da caixa forte" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Tempo esgotado" }, "lockNow": { - "message": "Bloquear agora" + "message": "Pechar agora" }, "lockAll": { - "message": "Bloquear todo" + "message": "Pechar todo" }, "immediately": { "message": "Inmediatamente" @@ -731,10 +724,10 @@ "message": "4 horas" }, "onLocked": { - "message": "Ao bloquear o sistema" + "message": "Ó bloquear o sistema" }, "onRestart": { - "message": "Ao reiniciar o navegador" + "message": "Ó reiniciar o navegador" }, "never": { "message": "Nunca" @@ -743,16 +736,16 @@ "message": "Seguridade" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Repetir o contrasinal mestre" }, "masterPassword": { - "message": "Master password" + "message": "Contrasinal mestre" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "O contrasinal mestre non pode ser recuperado se o esqueces!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Obter pista do contrasinal mestre" }, "errorOccurred": { "message": "Produciuse un erro" @@ -770,7 +763,7 @@ "message": "É preciso volver escribir o contrasinal mestre." }, "masterPasswordMinlength": { - "message": "O contrasinal mestre debe ter polo menos $VALUE$ caracteres de lonxitude.", + "message": "O contrasinal mestre debe ter polo menos $VALUE$ caracteres.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -780,19 +773,19 @@ } }, "masterPassDoesntMatch": { - "message": "A confirmación de contrasinal mestre non coincide." + "message": "O contrasinal mestre repetido non coincide." }, "newAccountCreated": { "message": "A túa nova conta foi creada! Podes iniciar sesión agora." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "A túa nova conta foi creada!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "A túa sesión foi iniciada!" }, "youSuccessfullyLoggedIn": { - "message": "Iniciaches sesión correctamente" + "message": "Sesión iniciada con éxito" }, "youMayCloseThisWindow": { "message": "Podes pechar está ventá" @@ -801,10 +794,10 @@ "message": "Enviámoste un correo electrónico coa túa pista de contrasinal mestre." }, "verificationCodeRequired": { - "message": "É preciso código de verificación." + "message": "É preciso un código de verificación." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "O proceso de autenticación foi cancelado ou levou moito tempo. Por favor, volve intentalo." }, "invalidVerificationCode": { "message": "Código de verificación non válido" @@ -820,28 +813,28 @@ } }, "autofillError": { - "message": "Incapaz de encher automaticamente o elemento seleccionado nesta páxina. Copia e pega a información no seu lugar." + "message": "Non se puido autoencher o formulario nesta páxina. Copia e pega a información manualmente." }, "totpCaptureError": { - "message": "Incapaz escanear o código QR da páxina web actual" + "message": "Non se puido escanear o código QR da páxina web" }, "totpCaptureSuccess": { "message": "Clave de autenticación engadida" }, "totpCapture": { - "message": "Escanea o código QR autenticador da páxina web actual" + "message": "Escanea o código QR do autenticador da páxina web" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "Fai a verificación en 2 pasos imperceptible" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden pode gardar e encher códigos de verificación en 2 pasos. Copia e pega a clave neste campo." }, "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." + "message": "Bitwarden pode gardar e encher códigos de verificación en 2 pasos. Preme a icona da cámara para extraer o código QR do autenticador desta web, ou copia e pega a clave neste campo." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Aprende máis sobre autenticadores." }, "copyTOTP": { "message": "Copiar clave de autenticación (TOTP)" @@ -850,31 +843,31 @@ "message": "Sesión pechada" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "A túa sesión foi pechada." }, "loginExpired": { "message": "A túa sesión caducou." }, "logIn": { - "message": "Log in" + "message": "Iniciar sesión" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Iniciar sesión en Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "Reiniciar rexistro" }, "expiredLink": { - "message": "Expired link" + "message": "Ligazón caducada" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "Por favor, reinicia o rexistro ou tenta iniciar sesión." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Pode que xa teñas unha conta" }, "logOutConfirmation": { - "message": "Are you sure you want to log out?" + "message": "Estás seguro de que queres pechar a sesión?" }, "yes": { "message": "Si" @@ -886,19 +879,19 @@ "message": "Produciuse un erro inesperado." }, "nameRequired": { - "message": "Requírese o nome." + "message": "Nome requirido." }, "addedFolder": { "message": "Cartafol engadido" }, "twoStepLoginConfirmation": { - "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "A verificación en 2 pasos (2FA) fai a túa conta máis segura requirindo confirmar o inicio de sesión con métodos como unha clave de seguridade, aplicación de autenticación, SMS, chamada telefónica ou correo electrónico. A 2FA pode configurarse na aplicación web. Queres ir agora?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Fai a túa conta máis segura activando a verificación en 2 pasos na aplicación web." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Ir á aplicación web?" }, "editedFolder": { "message": "Cartafol gardado" @@ -910,10 +903,10 @@ "message": "Cartafol eliminado" }, "gettingStartedTutorial": { - "message": "Tutorial introdutivo" + "message": "Titorial de Primeiros pasos" }, "gettingStartedTutorialVideo": { - "message": "Watch our getting started tutorial to learn how to get the most out of the browser extension." + "message": "Mira o noso titorial de Primeiros pasos para aprender a sacar partido á extensión de navegador." }, "syncingComplete": { "message": "Sincronización completa" @@ -941,32 +934,32 @@ "message": "Nova URI" }, "addDomain": { - "message": "Add domain", + "message": "Engadir dominio", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { - "message": "Elemento engadido" + "message": "Entrada engadida" }, "editedItem": { - "message": "Elemento gardado" + "message": "Entrada gardada" }, "deleteItemConfirmation": { - "message": "Realmente queres enviar ao lixo?" + "message": "Seguro que queres envialo ó lixo?" }, "deletedItem": { - "message": "Elemento enviado ao lixo" + "message": "Entrada enviada ó lixo" }, "overwritePassword": { "message": "Sobrescribir contrasinal" }, "overwritePasswordConfirmation": { - "message": "Estás seguro de que queres sobrescribir o contrasinal actual?" + "message": "Seguro que queres sobrescribir o contrasinal actual?" }, "overwriteUsername": { "message": "Sobrescribir nome de usuario" }, "overwriteUsernameConfirmation": { - "message": "Estás seguro de que queres sobrescribir o nome de usuario actual?" + "message": "Seguro que queres sobrescribir o nome de usuario actual?" }, "searchFolder": { "message": "Buscar cartafol" @@ -982,103 +975,106 @@ "description": "This is the folder for uncategorized items" }, "enableAddLoginNotification": { - "message": "Solicita engadir inicio de sesión" + "message": "Ofrecer gardar credenciais" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "Gardar nas opcións da caixa forte" }, "addLoginNotificationDesc": { - "message": "Ask to add an item if one isn't found in your vault." + "message": "Ofrecer gardar un elemento se non se atopa na caixa forte." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "Ofrecer gardar un elemento se non se atopa na caixa forte. Aplica a tódalas sesións iniciadas." }, "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "message": "Na caixa forte, amosar tarxetas como suxestións de Autoenchido" }, "showCardsCurrentTab": { - "message": "Amosar tarxetas no separador" + "message": "Amosar tarxetas na pestana" }, "showCardsCurrentTabDesc": { - "message": "Lista os elementos de tarxeta no separador para fácil auto-completado." + "message": "Lista na pestana actual as tarxetas gardadas para autoenchido." }, "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "message": "Na caixa forte, amosar identidades como suxestións de Autoenchido" }, "showIdentitiesCurrentTab": { - "message": "Mostrar identidades no separador" + "message": "Amosar identidades na pestana" }, "showIdentitiesCurrentTabDesc": { - "message": "Lista os elementos de identidade no separador para fácil auto-completado." + "message": "Lista na pestana actual as identidades gardadas para autoenchido." + }, + "clickToAutofillOnVault": { + "message": "Clicar nas entradas da caixa forte para autoencher" }, "clearClipboard": { - "message": "Limpar portapapeis", + "message": "Baleirar portapapeis", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { - "message": "Automatically clear copied values from your clipboard.", + "message": "Limpar automaticamente os valores copiados do teu portapapeis.", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "notificationAddDesc": { - "message": "Should Bitwarden remember this password for you?" + "message": "Debería Bitwarden lembrar este contrasinal?" }, "notificationAddSave": { "message": "Gardar" }, "enableChangedPasswordNotification": { - "message": "Ask to update existing login" + "message": "Ofrecer actualizar as credenciais xa gardadas" }, "changedPasswordNotificationDesc": { - "message": "Ask to update a login's password when a change is detected on a website." + "message": "Ofrecer actualizar o contrasinal dunha credencial ó detectar un cambio." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "Ofrecer actualizar o contrasinal dunha credencial ó detectar un cambio. Aplica a tódalas sesións iniciadas." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Ofrecer gardar e empregar Claves de acceso" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "Ofrecer gardar e empregar Claves de acceso na caixa forte. Aplica a tódalas sesións iniciadas." }, "notificationChangeDesc": { - "message": "Do you want to update this password in Bitwarden?" + "message": "Queres actualizar este contrasinal en Bitwarden?" }, "notificationChangeSave": { "message": "Actualizar" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the autofill request." + "message": "Abre a caixa forte de Bitwarden para poder continuar co autoenchido." }, "notificationUnlock": { - "message": "Desbloquear" + "message": "Abrir" }, "additionalOptions": { - "message": "Additional options" + "message": "Opcións adicionais" }, "enableContextMenuItem": { - "message": "Show context menu options" + "message": "Amosar opcións no menú contextual" }, "contextMenuItemDesc": { - "message": "Use a secondary click to access password generation and matching logins for the website." + "message": "Usar o clic dereito para acceder á xeración de contrasinais e autoenchido para a web." }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Usar o clic dereito para acceder á xeración de contrasinais e autoenchido para a web. Aplica a tódalas sesións iniciadas." }, "defaultUriMatchDetection": { - "message": "Default URI match detection", + "message": "Detección de coincidencias de URI por defecto", "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." + "message": "Selecciona o tipo de Detección de coincidencias de URI que se empregará por defecto para accións como o autoenchido." }, "theme": { "message": "Tema" }, "themeDesc": { - "message": "Change the application's color theme." + "message": "Cambiar o tema de cor da aplicación." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Cambiar o tema de cor da aplicación. Aplica a tódalas sesións iniciadas." }, "dark": { "message": "Escuro", @@ -1093,7 +1089,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Exportar dende" }, "exportVault": { "message": "Exportar caixa forte" @@ -1102,28 +1098,28 @@ "message": "Formato de ficheiro" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "O arquivo exportado estará protexido e requirirá o contrasinal do arquivo para descifralo." }, "filePassword": { - "message": "File password" + "message": "Contrasinal do arquivo" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Este contrasinal empregarase para exportar e importar o arquivo" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Empregar a clave de cifrado da túa conta, derivada do teu nome de usuario e contrasinal, para cifrar o arquivo exportado e limitar o importado á conta de usuario actual." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Crea un contrasinal para cifrar o arquivo exportado e importalo en calquera conta de Bitwarden empregando este contrasinal." }, "exportTypeHeading": { - "message": "Export type" + "message": "Tipo de exportado" }, "accountRestricted": { - "message": "Account restricted" + "message": "Conta restrinxida" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Contrasinal do arquivo\" e \"Repetir contrasinal do arquivo\" non coinciden." }, "warning": { "message": "ADVERTENCIA", @@ -1134,34 +1130,31 @@ "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { - "message": "Confirm vault export" + "message": "Confirmar o exportado da caixa forte" }, "exportWarningDesc": { - "message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." + "message": "Esta exportación contén os teus datos nun formato sen cifrar. Non deberías gardar ou enviar o arquivo por canais inseguros (como correo electrónico). Elimínao inmediatamente despois de finalizar o seu uso." }, "encExportKeyWarningDesc": { - "message": "This export encrypts your data using your account's encryption key. If you ever rotate your account's encryption key you should export again since you will not be able to decrypt this export file." + "message": "Este tipo de exportado cifra os teus datos empregando a clave de cifrado da túa conta. Se cambias o teu contrasinal ou nome de usuario deberás volver xerar o arquivo." }, "encExportAccountWarningDesc": { - "message": "Account encryption keys are unique to each Bitwarden user account, so you can't import an encrypted export into a different account." + "message": "As claves de cifrado son únicas para cada usuario, polo que non poderás importar un arquivo exportado desta maneira nunha conta distinta." }, "exportMasterPassword": { - "message": "Enter your master password to export your vault data." + "message": "Introduce o teu contrasinal mestre para exportar a caixa forte." }, "shared": { "message": "Compartido" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "Bitwarden para Empresas permite compartir entradas da caixa forte empregando unha organización. Aprende máis en bitwarden.com." }, "moveToOrganization": { - "message": "Move to organization" - }, - "share": { - "message": "Compartir" + "message": "Mover á organización" }, "movedItemToOrg": { - "message": "$ITEMNAME$ moved to $ORGNAME$", + "message": "$ITEMNAME$ moveuse a $ORGNAME$", "placeholders": { "itemname": { "content": "$1", @@ -1174,7 +1167,7 @@ } }, "moveToOrgDesc": { - "message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved." + "message": "Escolle a organización á que queres transferir esta entrada. Ó transferila, tamén transfires a súa propiedade á organización. Unha vez transferida, xa non serás o propietario directo desde elemento." }, "learnMore": { "message": "Máis información" @@ -1210,82 +1203,82 @@ "message": "Anexo gardado" }, "file": { - "message": "Ficheiro" + "message": "Arquivo" }, "fileToShare": { - "message": "File to share" + "message": "Arquivo a compartir" }, "selectFile": { - "message": "Selecciona un ficheiro" + "message": "Selecciona un arquivo" }, "maxFileSize": { - "message": "O tamaño máximo de ficheiro é de 500 MB." + "message": "O tamaño máximo é de 500 MB." }, "featureUnavailable": { - "message": "Característica non dispoñible" + "message": "Función non dispoñible" }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "Requírese mudar a clave de cifrado. Por favor, inicia sesión na aplicación web para actualizala." }, "premiumMembership": { - "message": "Membresía de luxo" + "message": "Plan Prémium" }, "premiumManage": { - "message": "Xestionar membresía" + "message": "Xestionar plan" }, "premiumManageAlert": { - "message": "You can manage your membership on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "Podes xestionar o teu plan na caixa forte web de bitwarden.com. Queres visitala agora?" }, "premiumRefresh": { - "message": "Refresh membership" + "message": "Actualizar o teu plan" }, "premiumNotCurrentMember": { - "message": "You are not currently a Premium member." + "message": "Actualmente non es usuario Prémium." }, "premiumSignUpAndGet": { - "message": "Sign up for a Premium membership and get:" + "message": "Actualiza a unha conta Premium e recibe:" }, "ppremiumSignUpStorage": { - "message": "1 GB encrypted storage for file attachments." + "message": "1 GB de almacenamento cifrado para arquivos anexos." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Acceso de emerxencia." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Opcións de verificación en 2 pasos privadas tales coma YubiKey ou Duo." }, "ppremiumSignUpReports": { - "message": "Password hygiene, account health, and data breach reports to keep your vault safe." + "message": "Limpeza de contrasinais, saúde de contas e informes de filtración de datos para manter a túa caixa forte segura." }, "ppremiumSignUpTotp": { - "message": "TOTP verification code (2FA) generator for logins in your vault." + "message": "Xerador de códigos TOTP (2FA) para as credenciais da túa caixa forte." }, "ppremiumSignUpSupport": { - "message": "Priority customer support." + "message": "Atención ó cliente prioritaria." }, "ppremiumSignUpFuture": { - "message": "All future Premium features. More coming soon!" + "message": "Tódalas funcións Prémium futuras!" }, "premiumPurchase": { - "message": "Purchase Premium" + "message": "Adquirir Prémium" }, "premiumPurchaseAlert": { - "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "Podes adquirir o plan Prémium na aplicación web de bitwarden.com. Queres visitala agora mesmo?" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "Podes adquirir o plan Prémium dende os axustes de conta da aplicación web de Bitwarden." }, "premiumCurrentMember": { - "message": "You are a Premium member!" + "message": "Xa es un usuario Prémium!" }, "premiumCurrentMemberThanks": { - "message": "Thank you for supporting Bitwarden." + "message": "Grazas por apoiar Bitwarden." }, "premiumFeatures": { - "message": "Upgrade to Premium and receive:" + "message": "Mellora a Prémium e recibe:" }, "premiumPrice": { - "message": "All for just $PRICE$ /year!", + "message": "Todo por só $PRICE$/ano!", "placeholders": { "price": { "content": "$1", @@ -1294,7 +1287,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "Todo por só $PRICE$ ó ano!", "placeholders": { "price": { "content": "$1", @@ -1303,34 +1296,34 @@ } }, "refreshComplete": { - "message": "Refresh complete" + "message": "Actualización completada" }, "enableAutoTotpCopy": { - "message": "Copy TOTP automatically" + "message": "Copiar TOTP automaticamente" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." + "message": "Se unha credencial ten clave de autenticación, copia o código TOTP no portapapeis ó autoencher os datos." }, "enableAutoBiometricsPrompt": { - "message": "Ask for biometrics on launch" + "message": "Requirir biometría no inicio" }, "premiumRequired": { - "message": "Premium required" + "message": "Plan Prémium requirido" }, "premiumRequiredDesc": { - "message": "A Premium membership is required to use this feature." + "message": "Requírese un plan Prémium para poder empregar esta función." }, "enterVerificationCodeApp": { - "message": "Enter the 6 digit verification code from your authenticator app." + "message": "Insire o código de 6 díxitos da túa aplicación de autenticación." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Tempo límite de autenticación superado" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "Superouse o tempo límite da sesión de autenticación. Recomeza o proceso." }, "enterVerificationCodeEmail": { - "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", + "message": "Insire o código de verificación de 6 díxitos enviado a $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -1339,7 +1332,7 @@ } }, "verificationCodeEmailSent": { - "message": "Verification email sent to $EMAIL$.", + "message": "Enviouse un correo de verificación a $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -1351,40 +1344,40 @@ "message": "Lémbrame" }, "sendVerificationCodeEmailAgain": { - "message": "Send verification code email again" + "message": "Enviar un novo correo de verificación" }, "useAnotherTwoStepMethod": { - "message": "Use another two-step login method" + "message": "Empregar outro método de verificación en 2 pasos" }, "insertYubiKey": { - "message": "Insert your YubiKey into your computer's USB port, then touch its button." + "message": "Conecta a túa YubiKey no porto USB, despois preme o seu botón." }, "insertU2f": { - "message": "Insert your security key into your computer's USB port. If it has a button, touch it." + "message": "Conecta a túa YubiKey no porto USB. Se ten un botón, prémeo." }, "webAuthnNewTab": { - "message": "To start the WebAuthn 2FA verification. Click the button below to open a new tab and follow the instructions provided in the new tab." + "message": "Para iniciar o proceso de verificación WebAuthn 2FA, clica no botón embaixo e segue as instrucións na pestana que se abrirá." }, "webAuthnNewTabOpen": { - "message": "Abrir novo separador" + "message": "Abrir nova pestana" }, "webAuthnAuthenticate": { "message": "Autenticar con WebAuthn" }, "loginUnavailable": { - "message": "Inicio de sesión non dispoñíbel" + "message": "Inicio de sesión non dispoñible" }, "noTwoStepProviders": { - "message": "This account has two-step login set up, however, none of the configured two-step providers are supported by this web browser." + "message": "Esta conta ten activada a verificación en 2 pasos, pero ningún dos métodos dispoñibles é compatible con este navegador." }, "noTwoStepProviders2": { - "message": "Please use a supported web browser (such as Chrome) and/or add additional providers that are better supported across web browsers (such as an authenticator app)." + "message": "Por favor, emprega un navegador compatible (como Chrome) ou un método de maior compatibilidade con distintos navegadores (como unha aplicación de autenticación)." }, "twoStepOptions": { - "message": "Opcións de inicio de sesión en dous pasos" + "message": "Opcións de verificación en dous pasos" }, "recoveryCodeDesc": { - "message": "Lost access to all of your two-factor providers? Use your recovery code to turn off all two-factor providers from your account." + "message": "Perdiche o acceso ós provedores de verificación en dous pasos (2FA)? Emprega o teu código de recuperación para desactivalos." }, "recoveryCodeTitle": { "message": "Código de recuperación" @@ -1393,61 +1386,61 @@ "message": "Aplicación de autenticación" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Insire un código xerado por unha aplicación de autenticación coma Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Clave de seguridade OTP de Yubico" }, "yubiKeyDesc": { - "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." + "message": "Emprega YubiKey para acceder á túa conta. Funciona con dispositivos YubiKey 4, 4 Nano, 4C e Neo." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Emprega un código xerado por Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", + "message": "Protexe a túa organización con Duo Security ca app móbil Duo Mobile, SMS, chamada telefónica ou chave de seguridade U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Use any WebAuthn compatible security key to access your account." + "message": "Emprega unha clave de seguridade compatible con WebAuthn para acceder á túa conta." }, "emailTitle": { "message": "Correo electrónico" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Insire o código enviado ó teu correo electrónico." }, "selfHostedEnvironment": { - "message": "Entorno de auto-aloxamento" + "message": "Entorno de aloxamento propio" }, "selfHostedEnvironmentFooter": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation." + "message": "Especifica a URL base do teu servidor Bitwarden de aloxamento propio." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Especifica a URL base do teu servidor Bitwarden de aloxamento propio. Ex.: https://bitwarden.compañia.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Para configuración avanzada, específica de forma independente a URL base de cada servizo." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Debes engadir ou a URL base do servidor ou, polo menos, un entorno personalizado." }, "customEnvironment": { "message": "Entorno personalizado" }, "customEnvironmentFooter": { - "message": "Para usuarios avanzados. Poder especificar o URL base de cada servizo de xeito independente." + "message": "Para usuarios avanzados. Podes especificar o URL base de cada servizo de xeito independente." }, "baseUrl": { "message": "URL do servidor" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL do servidor de aloxamento propio", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1469,101 +1462,101 @@ "message": "URLs do entorno gardadas" }, "showAutoFillMenuOnFormFields": { - "message": "Show autofill menu on form fields", + "message": "Amosar menú de autoenchido en formularios", "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Suxestións de autoenchido" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "Amosar suxestións de autoenchido en formularios" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "Amosar identidades como suxestións" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "Amosar tarxetas como suxestións" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "Amosar as suxestións ó premer a icona de Bitwarden" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "Aplica a tódalas sesións iniciadas." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser's built in password manager settings to avoid conflicts." + "message": "Desactiva o xestor de contrasinais do teu navegador para evitar conflitos." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "Cambiar axustes do navegador." }, "autofillOverlayVisibilityOff": { "message": "Apagado", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { - "message": "When field is selected (on focus)", + "message": "Cando o campo está seleccionado (activo)", "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When autofill icon is selected", + "message": "Cando a icona de autoenchido está seleccionada", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Autoencher ó cargar a páxina" }, "enableAutoFillOnPageLoad": { - "message": "Autofill on page load" + "message": "Autoencher ó cargar a páxina" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, autofill when the web page loads." + "message": "Se se detecta un formulario de inicio de sesión, autoenchelo ó cargar a páxina." }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit autofill on page load." + "message": "As webs comprometidas ou non fiables poden aproveitarse do autoenchido ó cargar a páxina." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Saber máis dos riscos" }, "learnMoreAboutAutofill": { - "message": "Learn more about autofill" + "message": "Saber máis do autoenchido" }, "defaultAutoFillOnPageLoad": { - "message": "Default autofill setting for login items" + "message": "Axustes de autoenchido por defecto para as credenciais" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off autofill on page load for individual login items from the item's Edit view." + "message": "Podes apagar o autoenchido para entradas concretas nas súas páxinas de edición." }, "itemAutoFillOnPageLoad": { - "message": "Autofill on page load (if set up in Options)" + "message": "Autoenchido ó cargar a páxina (se está activado nos Axustes)" }, "autoFillOnPageLoadUseDefault": { - "message": "Use default setting" + "message": "Usar axustes por defecto" }, "autoFillOnPageLoadYes": { - "message": "Autofill on page load" + "message": "Autoencher ó cargar a páxina" }, "autoFillOnPageLoadNo": { - "message": "Do not autofill on page load" + "message": "Non autoencher ó cargar a páxina" }, "commandOpenPopup": { - "message": "Open vault popup" + "message": "Abrir ventá emerxente da caixa forte" }, "commandOpenSidebar": { - "message": "Open vault in sidebar" + "message": "Abrir caixa forte na barra lateral" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Autoencher a última credencial empregada nesta web" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Autoencher a última tarxeta empregada nesta web" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Autoencher a última identidade empregada nesta web" }, "commandGeneratePasswordDesc": { - "message": "Generate and copy a new random password to the clipboard" + "message": "Xerar e copiar un novo contrasinal aleatorio ó portapapeis" }, "commandLockVaultDesc": { - "message": "Lock the vault" + "message": "Cerrar a caixa forte" }, "customFields": { "message": "Campos personalizados" @@ -1590,7 +1583,7 @@ "message": "Booleano" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Caixa" }, "cfTypeLinked": { "message": "Vinculado", @@ -1601,28 +1594,28 @@ "description": "This describes a value that is 'linked' (tied) to another value." }, "popup2faCloseMessage": { - "message": "Clicking outside the popup window to check your email for your verification code will cause this popup to close. Do you want to open this popup in a new window so that it does not close?" + "message": "Se clicas fóra desta ventá emerxente para comprobar o código do correo, cerrarase. Queres convertila nunha nova ventá completa para que non se cerre?" }, "popupU2fCloseMessage": { - "message": "This browser cannot process U2F requests in this popup window. Do you want to open this popup in a new window so that you can log in using U2F?" + "message": "Este navegador non pode procesar peticións U2F nesta ventá emerxente. Queres convertila nunha ventá completa para poder continuar?" }, "enableFavicon": { - "message": "Show website icons" + "message": "Amosar iconas web" }, "faviconDesc": { - "message": "Show a recognizable image next to each login." + "message": "Amosar unha imaxe recoñecible xunto a cada credencial." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Amosar unha imaxe recoñecible xunto a cada credencial. Aplica a tódalas sesións iniciadas en Bitwarden." }, "enableBadgeCounter": { - "message": "Show badge counter" + "message": "Amosar contador na icona" }, "badgeCounterDesc": { - "message": "Indicate how many logins you have for the current web page." + "message": "Indicar cantas credenciais tes gardadas para a web aberta." }, "cardholderName": { - "message": "Nome do titular da tarxeta" + "message": "Titular da tarxeta" }, "number": { "message": "Número" @@ -1631,7 +1624,7 @@ "message": "Marca" }, "expirationMonth": { - "message": "Mes de expiración" + "message": "Mes de vencemento" }, "expirationYear": { "message": "Ano de vencemento" @@ -1697,7 +1690,7 @@ "message": "Dr" }, "mx": { - "message": "Mx" + "message": "Srx" }, "firstName": { "message": "Nome" @@ -1712,19 +1705,19 @@ "message": "Nome completo" }, "identityName": { - "message": "Nome de identidade" + "message": "Nome da identidade" }, "company": { "message": "Empresa" }, "ssn": { - "message": "Número da seguridade social" + "message": "Número da Seguridade Social" }, "passportNumber": { "message": "Número de pasaporte" }, "licenseNumber": { - "message": "Número de licencia" + "message": "Número de matrícula" }, "email": { "message": "Correo electrónico" @@ -1763,7 +1756,7 @@ "message": "Inicio de sesión" }, "typeLogins": { - "message": "Inicios se sesión" + "message": "Inicios de sesión" }, "typeSecureNote": { "message": "Nota segura" @@ -1775,10 +1768,10 @@ "message": "Identidade" }, "typeSshKey": { - "message": "SSH key" + "message": "Clave SSH" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Novo $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1787,7 +1780,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Modificar $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1796,7 +1789,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Ver $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1808,13 +1801,13 @@ "message": "Historial de contrasinais" }, "generatorHistory": { - "message": "Generator history" + "message": "Historial do xerador" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Baleirar historial do xerador" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Se continúas, tódalas entradas serán eliminadas permanentemente do historial do xerador. Seguro que queres continuar?" }, "back": { "message": "Atrás" @@ -1823,7 +1816,7 @@ "message": "Coleccións" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ coleccións", "placeholders": { "count": { "content": "$1", @@ -1835,7 +1828,7 @@ "message": "Favoritos" }, "popOutNewWindow": { - "message": "Pop out to a new window" + "message": "Sacar a unha nova ventá" }, "refresh": { "message": "Actualizar" @@ -1847,23 +1840,23 @@ "message": "Identidades" }, "logins": { - "message": "Inicios de sesión" + "message": "Credenciais" }, "secureNotes": { "message": "Notas seguras" }, "sshKeys": { - "message": "SSH Keys" + "message": "Claves SSH" }, "clear": { "message": "Limpar", "description": "To clear something out. example: To clear browser history." }, "checkPassword": { - "message": "Check if password has been exposed." + "message": "Comprobar se o contrasinal foi filtrado." }, "passwordExposed": { - "message": "This password has been exposed $VALUE$ time(s) in data breaches. You should change it.", + "message": "Este contrasinal foi filtrado $VALUE$ vez/veces. Deberías cambialo.", "placeholders": { "value": { "content": "$1", @@ -1872,14 +1865,14 @@ } }, "passwordSafe": { - "message": "This password was not found in any known data breaches. It should be safe to use." + "message": "Este contrasinal non foi atopado en ningunha filtración de datos. Debería ser seguro." }, "baseDomain": { "message": "Dominio base", "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "Dominio base (recomendado)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1887,7 +1880,7 @@ "description": "Domain name. Ex. website.com" }, "host": { - "message": "Anfitrión", + "message": "Servidor", "description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'." }, "exact": { @@ -1901,18 +1894,18 @@ "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { - "message": "Match detection", + "message": "Detección de coincidencias", "description": "URI match detection for autofill." }, "defaultMatchDetection": { - "message": "Default match detection", + "message": "Detección de coincidencias por defecto", "description": "Default URI match detection for autofill." }, "toggleOptions": { - "message": "Toggle options" + "message": "Alternar opcións" }, "toggleCurrentUris": { - "message": "Toggle current URIs", + "message": "Alternar URIs actuais", "description": "Toggle the display of the URIs of the currently open tabs in the browser." }, "currentUri": { @@ -1927,19 +1920,19 @@ "message": "Tipos" }, "allItems": { - "message": "Todos os elementos" + "message": "Todas as entradas" }, "noPasswordsInList": { "message": "Non hai contrasinais que listar." }, "clearHistory": { - "message": "Clear history" + "message": "Baleirar historial" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Nada que amosar" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Non xerache nada recentemente" }, "remove": { "message": "Eliminar" @@ -1960,26 +1953,26 @@ "description": "ex. Date this password was updated" }, "neverLockWarning": { - "message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected." + "message": "Seguro que queres marcar a opción \"Nunca\"? Isto almacenará a clave de cifrado da túa caixa forte no teu dispositivo. Se usas esta opción debes asegurarte de ter o dispositivo ben protexido." }, "noOrganizationsList": { - "message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." + "message": "Non pertences a ningunha organización. As organizacións permiten compartir entradas con outros usuarios." }, "noCollectionsInList": { - "message": "There are no collections to list." + "message": "Non hai coleccións que listar." }, "ownership": { "message": "Propiedade" }, "whoOwnsThisItem": { - "message": "Quen posúe este elemento?" + "message": "Quen posúe esta entrada?" }, "strong": { "message": "Forte", "description": "ex. A strong password. Scale: Weak -> Good -> Strong" }, "good": { - "message": "Boa", + "message": "Bo", "description": "ex. A good password. Scale: Weak -> Good -> Strong" }, "weak": { @@ -1990,94 +1983,91 @@ "message": "Contrasinal mestre feble" }, "weakMasterPasswordDesc": { - "message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?" + "message": "O contrasinal mestre creado é feble. Debes empregar un contrasinal ou frase de contrasinal mestre forte para protexer debidamente a túa conta. Seguro que queres continuar con este contrasinal?" }, "pin": { "message": "PIN", "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." }, "unlockWithPin": { - "message": "Unlock with PIN" + "message": "Abrir con PIN" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "Definir PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "Crear PIN" }, "setYourPinCode": { - "message": "Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application." + "message": "Crea un PIN para abrir a caixa forte. Se algunha vez pechas a sesión en Bitwarden perderase a configuración deste PIN." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "O PIN empregarase no lugar do contrasinal mestre para abrir a caixa forte. Se pechas a sesión en Bitwarden perderase a configuración do PIN." }, "pinRequired": { - "message": "PIN code is required." + "message": "PIN requirido." }, "invalidPin": { - "message": "Invalid PIN code." + "message": "PIN incorrecto." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Múltiples intentos fallidos de PIN. Pechando a sesión." }, "unlockWithBiometrics": { - "message": "Unlock with biometrics" + "message": "Abrir con biometría" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Abrir con contrasinal mestre" }, "awaitDesktop": { - "message": "Awaiting confirmation from desktop" + "message": "Agardando pola confirmación do escritorio" }, "awaitDesktopDesc": { - "message": "Please confirm using biometrics in the Bitwarden desktop application to set up biometrics for browser." + "message": "Por favor confirma o uso de biometría na aplicación de escritorio de Bitwarden para activar a biometría no navegador." }, "lockWithMasterPassOnRestart": { - "message": "Lock with master password on browser restart" + "message": "Bloquear con contrasinal mestre ó pechar o navegador" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Requirir contrasinal mestre ó abrir o navegador" }, "selectOneCollection": { - "message": "You must select at least one collection." + "message": "Debes seleccionar polo menos unha colección." }, "cloneItem": { - "message": "Clonar elemento" + "message": "Duplicar entrada" }, "clone": { "message": "Clonar" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { - "message": "Password generator" + "message": "Xerador de contrasinais" }, "usernameGenerator": { - "message": "Username generator" + "message": "Xerador de nomes de usuario" }, "useThisPassword": { - "message": "Use this password" + "message": "Usar este contrasinal" }, "useThisUsername": { - "message": "Use this username" + "message": "Usar este nome de usuario" }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "Contrasinal seguro xerado! Non esquezas actualizar o contrasinal na web." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Usar o xerador", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "para crear un contrasinal forte", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { - "message": "Vault timeout action" + "message": "Acción do temporizador da caixa forte" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "Acción do temporizador" }, "lock": { "message": "Bloquear", @@ -2088,55 +2078,55 @@ "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { - "message": "Search trash" + "message": "Buscar no lixo" }, "permanentlyDeleteItem": { - "message": "Permanently delete item" + "message": "Eliminar entrada permanentemente" }, "permanentlyDeleteItemConfirmation": { - "message": "Are you sure you want to permanently delete this item?" + "message": "Seguro que queres eliminar permanentemente esta entrada?" }, "permanentlyDeletedItem": { - "message": "Item permanently deleted" + "message": "Entrada eliminada permanente" }, "restoreItem": { - "message": "Restore item" + "message": "Restaurar entrada" }, "restoredItem": { - "message": "Item restored" + "message": "Entrada restaurada" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Xa tes unha conta?" }, "vaultTimeoutLogOutConfirmation": { - "message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?" + "message": "Cerrar a sesión despois dun tempo elimina o acceso á caixa forte ata que se volva iniciar sesión. Seguro que queres empregar esta opción?" }, "vaultTimeoutLogOutConfirmationTitle": { - "message": "Timeout action confirmation" + "message": "Confirmación de acción do temporizador" }, "autoFillAndSave": { - "message": "Autofill and save" + "message": "Autoencher e gardar" }, "fillAndSave": { - "message": "Fill and save" + "message": "Encher e gardar" }, "autoFillSuccessAndSavedUri": { - "message": "Item autofilled and URI saved" + "message": "Entrada autoenchida e URI gardada" }, "autoFillSuccess": { - "message": "Item autofilled " + "message": "Entrada autoenchida " }, "insecurePageWarning": { - "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." + "message": "Aviso: esta é unha páxina web HTTP sen cifrar, e calquera información que envíes é visible para terceiros. A credencial foi orixinalmente gardada nunha web HTTPS cifrada." }, "insecurePageWarningFillPrompt": { - "message": "Do you still wish to fill this login?" + "message": "Aínda queres autoencher esta credencial?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." + "message": "O formulario está aloxado nun dominio diferente da URI que gardaches. Clica Aceptar para autoencher igualmente, ou Cancelar para parar." }, "autofillIframeWarningTip": { - "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", + "message": "Para prever este aviso no futuro, garda este URI, $HOSTNAME$, na túa entrada en Bitwarden para este sitio.", "placeholders": { "hostname": { "content": "$1", @@ -2145,22 +2135,22 @@ } }, "setMasterPassword": { - "message": "Set master password" + "message": "Definir contrasinal mestre" }, "currentMasterPass": { - "message": "Current master password" + "message": "Contrasinal mestre actual" }, "newMasterPass": { - "message": "New master password" + "message": "Novo contrasinal mestre" }, "confirmNewMasterPass": { - "message": "Confirm new master password" + "message": "Repetir novo contrasinal mestre" }, "masterPasswordPolicyInEffect": { - "message": "One or more organization policies require your master password to meet the following requirements:" + "message": "As directivas da túa organización esixen que o teu contrasinal mestre cumpra os seguintes requisitos:" }, "policyInEffectMinComplexity": { - "message": "Minimum complexity score of $SCORE$", + "message": "Complexidade mínima de $SCORE$ puntos", "placeholders": { "score": { "content": "$1", @@ -2169,7 +2159,7 @@ } }, "policyInEffectMinLength": { - "message": "Minimum length of $LENGTH$", + "message": "Mínimo de $LENGTH$ caracteres", "placeholders": { "length": { "content": "$1", @@ -2178,16 +2168,16 @@ } }, "policyInEffectUppercase": { - "message": "Contain one or more uppercase characters" + "message": "Conter polo menos unha maiúscula" }, "policyInEffectLowercase": { - "message": "Contain one or more lowercase characters" + "message": "Conter polo menos unha minúscula" }, "policyInEffectNumbers": { - "message": "Contain one or more numbers" + "message": "Conter polo menos un número" }, "policyInEffectSpecial": { - "message": "Contain one or more of the following special characters $CHARS$", + "message": "Conter polo menos un dos seguintes caracteres: $CHARS$", "placeholders": { "chars": { "content": "$1", @@ -2196,155 +2186,167 @@ } }, "masterPasswordPolicyRequirementsNotMet": { - "message": "Your new master password does not meet the policy requirements." + "message": "Este contrasinal mestre non cumpre cas directivas." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "Recibe consellos, anuncios e oportunidades de investigación de Bitwarden no teu correo." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Cancelar subscrición" }, "atAnyTime": { - "message": "at any time." + "message": "en calquera momento." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Ó continuar, estarás aceptando os" }, "and": { - "message": "and" + "message": "e" }, "acceptPolicies": { - "message": "By checking this box you agree to the following:" + "message": "Ó marcar esta caixa aceptas o seguinte:" }, "acceptPoliciesRequired": { - "message": "Terms of Service and Privacy Policy have not been acknowledged." + "message": "Os Termos de Servizo e a Política de Privacidade non foron aceptados." }, "termsOfService": { - "message": "Terms of Service" + "message": "Termos de Servizo" }, "privacyPolicy": { - "message": "Privacy Policy" + "message": "Política de Privacidade" }, "hintEqualsPassword": { - "message": "Your password hint cannot be the same as your password." + "message": "A pista do contrasinal non pode ser o contrasinal." }, "ok": { - "message": "Ok" + "message": "Aceptar" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Erro de actualización do Token de Acceso" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Non se atoparon token de actualización nin claves da API. Por favor trata de volver iniciar sesión." }, "desktopSyncVerificationTitle": { - "message": "Desktop sync verification" + "message": "Verificación de sincronización co escritorio" }, "desktopIntegrationVerificationText": { - "message": "Please verify that the desktop application shows this fingerprint: " + "message": "Por favor verifica que a app de escritorio amosa esta pegada dixital: " }, "desktopIntegrationDisabledTitle": { - "message": "Browser integration is not set up" + "message": "Integración co navegador non configurada" }, "desktopIntegrationDisabledDesc": { - "message": "Browser integration is not set up in the Bitwarden desktop application. Please set it up in the settings within the desktop application." + "message": "A integración co navegador non está activada na app de escritorio de Bitwarden. Por favor configúraa dende os axustes da app de escritorio." }, "startDesktopTitle": { - "message": "Start the Bitwarden desktop application" + "message": "Abrir a app de escritorio de Bitwarden" }, "startDesktopDesc": { - "message": "The Bitwarden desktop application needs to be started before unlock with biometrics can be used." + "message": "Para poder empregar a biometría a aplicación de escritorio de Bitwarden debe estar executada." }, "errorEnableBiometricTitle": { - "message": "Unable to set up biometrics" + "message": "Non foi posible activar a biometría" }, "errorEnableBiometricDesc": { - "message": "Action was canceled by the desktop application" + "message": "Proceso interrompido pola aplicación de escritorio" }, "nativeMessagingInvalidEncryptionDesc": { - "message": "Desktop application invalidated the secure communication channel. Please retry this operation" + "message": "A aplicación de escritorio invalidou a canle de comunicación segura. Por favor volve intentalo" }, "nativeMessagingInvalidEncryptionTitle": { - "message": "Desktop communication interrupted" + "message": "Comunicación co escritorio interrompida" }, "nativeMessagingWrongUserDesc": { - "message": "The desktop application is logged into a different account. Please ensure both applications are logged into the same account." + "message": "A aplicación de escritorio ten unha conta distinta. Por favor asegúrate de que ambas aplicacións iniciaron sesión na mesma conta." }, "nativeMessagingWrongUserTitle": { - "message": "Account missmatch" + "message": "Conta non coincidente" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "Clave biométrica non coincidente" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "Desbloqueo biométrico fallido. A clave biométrica non puido abrir a caixa forte. Por favor proba a volver configurar a biometría." }, "biometricsNotEnabledTitle": { - "message": "Biometrics not set up" + "message": "Biometría non activada" }, "biometricsNotEnabledDesc": { - "message": "Browser biometrics requires desktop biometric to be set up in the settings first." + "message": "A biometría no navegador require que se configure primeiro na app de escritorio." }, "biometricsNotSupportedTitle": { - "message": "Biometrics not supported" + "message": "Biometría non compatible" }, "biometricsNotSupportedDesc": { - "message": "Browser biometrics is not supported on this device." + "message": "Biometría do navegador non compatible con este dispositivo." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "Usuario cerrado ou desconectado" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "Por favor desbloquea o usuario na aplicación de escritorio e volve intentalo." }, "biometricsNotAvailableTitle": { - "message": "Biometric unlock unavailable" + "message": "Desbloqueo biométrico non dispoñible" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "Desbloqueo biométrico non dispoñible neste momento. Volve intentalo máis tarde." }, "biometricsFailedTitle": { - "message": "Biometrics failed" + "message": "Fallo da biometría" }, "biometricsFailedDesc": { - "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + "message": "A biometría non puido ser completada. Proba a usar o contrasinal mestre ou pechar a sesión. Se o problema persiste, contacta con atención ó cliente." }, "nativeMessaginPermissionErrorTitle": { - "message": "Permission not provided" + "message": "Permiso non concedido" }, "nativeMessaginPermissionErrorDesc": { - "message": "Without permission to communicate with the Bitwarden Desktop Application we cannot provide biometrics in the browser extension. Please try again." + "message": "Sen permiso de comunicación ca aplicación de escritorio non podemos prover biometría na extensión de navegador. Por favor, volve intentalo." }, "nativeMessaginPermissionSidebarTitle": { - "message": "Permission request error" + "message": "Error de petición de permisos" }, "nativeMessaginPermissionSidebarDesc": { - "message": "This action cannot be done in the sidebar, please retry the action in the popup or popout." + "message": "Esta acción non pode ser realizada na barra lateral. Inténtao dende a ventá emerxente." }, "personalOwnershipSubmitError": { - "message": "Due to an Enterprise Policy, you are restricted from saving items to your personal vault. Change the Ownership option to an organization and choose from available collections." + "message": "Debido a unha directiva da empresa, non podes gardar entradas na túa caixa forte. Cambia a opción de propiedade a unha organización e elixe unha das coleccións dispoñibles." }, "personalOwnershipPolicyInEffect": { - "message": "An organization policy is affecting your ownership options." + "message": "Unha directiva da empresa está a afectar ás túas opcións de propiedade." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Unha directiva da empresa impide importar entradas á túa caixa forte individual." }, "domainsTitle": { - "message": "Domains", + "message": "Dominios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Dominios bloqueados" + }, "excludedDomains": { - "message": "Excluded domains" + "message": "Dominios excluídos" }, "excludedDomainsDesc": { - "message": "Bitwarden will not ask to save login details for these domains. You must refresh the page for changes to take effect." + "message": "Bitwarden non ofrecerá gardar contas para estes dominios. Recarga a páxina para que os cambios fagan efecto." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "Bitwarden non ofrecerá gardar contas para estes dominios en ningunha das sesións iniciadas. Recarga a páxina para que os cambios fornezan efecto." + }, + "blockedDomainsDesc": { + "message": "O autoenchido e outras funcións relacionadas non estarán dispoñibles para estas webs. Debes recargar a páxina para que os cambios teñan efecto." + }, + "autofillBlockedNoticeV2": { + "message": "O autoenchido está bloqueado para esta web." + }, + "autofillBlockedNoticeGuidance": { + "message": "Cambia isto en axustes" }, "websiteItemLabel": { - "message": "Website $number$ (URI)", + "message": "Web $number$ (URI)", "placeholders": { "number": { "content": "$1", @@ -2353,7 +2355,7 @@ } }, "excludedDomainsInvalidDomain": { - "message": "$DOMAIN$ is not a valid domain", + "message": "$DOMAIN$ non é un dominio válido", "placeholders": { "domain": { "content": "$1", @@ -2361,18 +2363,21 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Dominios bloqueados gardados" + }, "excludedDomainsSavedSuccess": { - "message": "Excluded domain changes saved" + "message": "Dominios excluídos gardados" }, "limitSendViews": { - "message": "Limit views" + "message": "Limitar visionados" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Ninguén pode ver este Send dende que se alcance o límite.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ visionados restantes", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2386,22 +2391,14 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Send details", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "searchSends": { - "message": "Buscar Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Engadir Send", + "message": "Detalles do Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { "message": "Texto" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Texto a compartir" }, "sendTypeFile": { "message": "Ficheiro" @@ -2411,36 +2408,29 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Hide text by default" - }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." + "message": "Ocultar texto por defecto" }, "expired": { - "message": "Caducado" - }, - "pendingDeletion": { - "message": "Pending deletion" + "message": "Vencido" }, "passwordProtected": { - "message": "Password protected" + "message": "Protexido con contrasinal" }, "copyLink": { - "message": "Copy link" + "message": "Copiar ligazón" }, "copySendLink": { "message": "Copiar ligazón Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { - "message": "Remove Password" + "message": "Eliminar contrasinal" }, "delete": { "message": "Eliminar" }, "removedPassword": { - "message": "Password removed" + "message": "Contrasinal eliminado" }, "deletedSend": { "message": "Send eliminado", @@ -2451,55 +2441,36 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "disabled": { - "message": "Disabled" + "message": "Deshabilitado" }, "removePasswordConfirmation": { - "message": "Are you sure you want to remove the password?" + "message": "Seguro que queres eliminar o contrasinal?" }, "deleteSend": { "message": "Eliminar Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendConfirmation": { - "message": "Are you sure you want to delete this Send?", + "message": "Seguro que queres eliminar este Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "Seguro que queres eliminar permanentemente este Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { "message": "Editar Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { - "message": "Deletion date" - }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + "message": "Data de eliminación" }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "Este Send será permanente eliminado nesta data.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Expiration date" - }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + "message": "Data de vencemento" }, "oneDay": { "message": "1 día" @@ -2516,43 +2487,10 @@ "custom": { "message": "Personalizado" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Engade un contrasinal opcional para os destinatarios deste Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "Novo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2565,7 +2503,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { - "message": "Due to an enterprise policy, you are only able to delete an existing Send.", + "message": "Por unha directiva de empresa só podes eliminar un Send existente.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { @@ -2573,15 +2511,15 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "Send creado con éxito!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "O Send estará dispoñible para calquera ca ligazón durante 1 hora.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "O Send estará dispoñible para calquera ca ligazón durante $HOURS$ horas.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2591,11 +2529,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "O Send estará dispoñible para calquera ca ligazón durante 1 día.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "O Send estará dispoñible para calquera ca ligazón durante $DAYS$ días.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2605,7 +2543,7 @@ } }, "sendLinkCopied": { - "message": "Send link copied", + "message": "Ligazón do Send copiado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { @@ -2613,120 +2551,102 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogText": { - "message": "Pop out extension?", + "message": "Sacar a extensión?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out the extension to a new window.", + "message": "Para crear un arquivo Send, necesitas sacar a extensión a unha nova ventá.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { - "message": "In order to choose a file, open the extension in the sidebar (if possible) or pop out to a new window by clicking this banner." + "message": "Para escoller un arquivo, abre a extensión na barra lateral (se é posible) ou sácaa a unha nova ventá premendo este botón." }, "sendFirefoxFileWarning": { - "message": "In order to choose a file using Firefox, open the extension in the sidebar or pop out to a new window by clicking this banner." + "message": "Para escoller un arquivo empregando Firefox, abre a extensión na barra lateral ou sácaa a unha nova ventá premendo este botón." }, "sendSafariFileWarning": { - "message": "In order to choose a file using Safari, pop out to a new window by clicking this banner." + "message": "Para escoller un arquivo empregando Safari, saca a extensión a unha nova ventá premendo este botón." }, "popOut": { - "message": "Pop out" + "message": "Sacar" }, "sendFileCalloutHeader": { - "message": "Before you start" - }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" + "message": "Antes de comezar" }, "expirationDateIsInvalid": { - "message": "The expiration date provided is not valid." + "message": "A data de vencemento non é válida." }, "deletionDateIsInvalid": { - "message": "The deletion date provided is not valid." + "message": "A data de eliminación non é válida." }, "expirationDateAndTimeRequired": { - "message": "An expiration date and time are required." + "message": "Unha data e hora de vencemento son requiridas." }, "deletionDateAndTimeRequired": { - "message": "A deletion date and time are required." + "message": "Unha data e hora de eliminación son requiridas." }, "dateParsingError": { - "message": "There was an error saving your deletion and expiration dates." - }, - "hideEmail": { - "message": "Hide my email address from recipients." + "message": "Produciuse un erro ó gardar as datas de vencemento e eliminación." }, "hideYourEmail": { - "message": "Hide your email address from viewers." - }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." + "message": "Oculta o teu enderezo electrónico ós visitantes." }, "passwordPrompt": { - "message": "Master password re-prompt" + "message": "Volver solicitar o contrasinal mestre" }, "passwordConfirmation": { - "message": "Master password confirmation" + "message": "Repetir o contrasinal mestre" }, "passwordConfirmationDesc": { - "message": "This action is protected. To continue, please re-enter your master password to verify your identity." + "message": "Esta acción está protexida. Para continuar por favor volve introducir o teu contrasinal mestre." }, "emailVerificationRequired": { - "message": "Email verification required" + "message": "Verificación do correo electrónico requirida" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Correo electrónico verificado" }, "emailVerificationRequiredDesc": { - "message": "You must verify your email to use this feature. You can verify your email in the web vault." + "message": "Debes verificar o teu correo electrónico para empregar esta función. Podes facelo dende a aplicación web." }, "updatedMasterPassword": { - "message": "Updated master password" + "message": "Contrasinal mestre actualizado" }, "updateMasterPassword": { - "message": "Update master password" + "message": "Actualizar contrasinal mestre" }, "updateMasterPasswordWarning": { - "message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "O teu contrasinal mestre foi recentemente cambiado por un administrador da túa organización. Para poder acceder á caixa forte debes actualizalo. A continuación cerrarase a túa sesión, requirindo volver iniciala. As sesións activas noutros dispositivos poden permanecer activas por unha hora." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "O teu contrasinal mestre non cumpre algún dos requisitos das directivas da empresa. Para poder acceder a caixa forte debes actualizar o teu contrasinal mestre. A continuación cerrarase a túa sesión, requirindo volver iniciala. As sesións activas noutros dispositivos poden permanecer activas por unha hora." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "A túa organización desactivou cifrado en dispositivos de confianza. Por favor, crea un contrasinal mestre para acceder á túa caixa forte." }, "resetPasswordPolicyAutoEnroll": { - "message": "Automatic enrollment" + "message": "Rexistro automático" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." + "message": "Esta organización ten unha directiva de empresa que te rexistrará automaticamente nun restablecemento de contrasinal. O rexistro permitirá a organización cambiar o teu contrasinal mestre." }, "selectFolder": { - "message": "Select folder..." + "message": "Seleccionar cartafol..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Cartafol non atopado", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Os permisos da organización foron actualizados, requirindo que crees un contrasinal mestre.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "A túa urbanización require que crees un contrasinal mestre.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "de $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2735,7 +2655,7 @@ } }, "verificationRequired": { - "message": "Verification required", + "message": "Verificación requirida", "description": "Default title for the user verification dialog." }, "hours": { @@ -2745,10 +2665,10 @@ "message": "Minutos" }, "vaultTimeoutPolicyAffectingOptions": { - "message": "Enterprise policy requirements have been applied to your timeout options" + "message": "Directivas da empresa foron aplicadas ás opcións do temporizador da túa Caixa forte" }, "vaultTimeoutPolicyInEffect": { - "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "message": "Directivas da empresa restrinxen o temporizador da túa Caixa forte a un máximo de $HOURS$ hora(s) e $MINUTES$ minuto(s).", "placeholders": { "hours": { "content": "$1", @@ -2761,7 +2681,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "$HOURS$ hora(s) e $MINUTES$ minuto(s) de máximo.", "placeholders": { "hours": { "content": "$1", @@ -2774,7 +2694,7 @@ } }, "vaultTimeoutPolicyMaximumError": { - "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "message": "O tempo indicado excede as restricións definidas pola túa organización: un máximo de $HOURS$ hora(s) e $MINUTES$ minuto(s)", "placeholders": { "hours": { "content": "$1", @@ -2787,7 +2707,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "As directivas da empresa restrinxen o temporizador da túa Caixa forte a $HOURS$ hora(s) e $MINUTES$ minuto(s) de máximo. A acción a realizar será: $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -2804,7 +2724,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "As directivas da empresa definiron o temporizador da túa Caixa forte en $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2813,22 +2733,22 @@ } }, "vaultTimeoutTooLarge": { - "message": "Your vault timeout exceeds the restrictions set by your organization." + "message": "O temporizador da túa Caixa forte excede as restricións definidas pola túa organización." }, "vaultExportDisabled": { - "message": "Vault export unavailable" + "message": "Exportado da Caixa forte non dispoñible" }, "personalVaultExportPolicyInEffect": { - "message": "One or more organization policies prevents you from exporting your individual vault." + "message": "Directivas da empresa impiden exportar a túa caixa forte individual." }, "copyCustomFieldNameInvalidElement": { - "message": "Unable to identify a valid form element. Try inspecting the HTML instead." + "message": "No se puido identificar un elemento de formulario válido. Intenta inspeccionar no HTML en seu lugar." }, "copyCustomFieldNameNotUnique": { - "message": "No unique identifier found." + "message": "Non se atopou ningún identificador único." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", + "message": "$ORGANIZATION$ emprega SSO con un servidor de claves propio. Os membros da organización xa non precisan dun contrasinal mestre para iniciar sesión.", "placeholders": { "organization": { "content": "$1", @@ -2837,31 +2757,31 @@ } }, "leaveOrganization": { - "message": "Leave organization" + "message": "Deixar a organización" }, "removeMasterPassword": { - "message": "Remove master password" + "message": "Eliminar contrasinal mestre" }, "removedMasterPassword": { - "message": "Master password removed" + "message": "Contrasinal mestre eliminado" }, "leaveOrganizationConfirmation": { - "message": "Are you sure you want to leave this organization?" + "message": "Estás seguro de que queres deixar esta organización?" }, "leftOrganization": { - "message": "You have left the organization." + "message": "Deixache a organización." }, "toggleCharacterCount": { - "message": "Toggle character count" + "message": "Alternar contador de caracteres" }, "sessionTimeout": { - "message": "Your session has timed out. Please go back and try logging in again." + "message": "O temporizador da sesión rematou. Por favor, volve iniciar a sesión." }, "exportingPersonalVaultTitle": { - "message": "Exporting individual vault" + "message": "Exportando caixa forte individual" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Só se exportarán as entradas persoais da caixa forte asociada a $EMAIL$. As entradas da Caixa forte da organización non se incluirán. Tampouco se incluirá ningún arquivo anexo ás entradas.", "placeholders": { "email": { "content": "$1", @@ -2870,10 +2790,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Exportar Caixa forte da organización" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Só se exportará a Caixa forte asociada a $ORGANIZATION$. As entradas en Caixas fortes individuais ou doutras organizacións non se incluirán.", "placeholders": { "organization": { "content": "$1", @@ -2884,17 +2804,28 @@ "error": { "message": "Erro" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Erro de descifrado" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden non puido descifrar os seguintes elementos." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacto co cliente exitoso", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "para evitar a perda de datos.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { - "message": "Generate username" + "message": "Xerar nome de usuario" }, "generateEmail": { - "message": "Generate email" + "message": "Xerar enderezo electrónico" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "O valor debe estar entre $MIN$ e $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2908,7 +2839,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": "Usar $RECOMMENDED$ caracteres ou máis para xerar un contrasinal seguro.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2918,7 +2849,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": "Usar $RECOMMENDED$ palabras ou máis para xerar unha frase de contrasinal segura.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2927,21 +2858,18 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { - "message": "Plus addressed email", + "message": "Correo con alias", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "Usar as funcións de subdireccionamento do teu provedor de correo." }, "catchallEmail": { - "message": "Catch-all email" + "message": "Correo \"catch-all\"" }, "catchallEmailDesc": { - "message": "Use your domain's configured catch-all inbox." + "message": "Usar a caixa de entrada \"catch-all\" configurada para o teu dominio." }, "random": { "message": "Aleatorio" @@ -2952,31 +2880,25 @@ "websiteName": { "message": "Nome do sitio web" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { - "message": "Service" + "message": "Servizo" }, "forwardedEmail": { - "message": "Forwarded email alias" + "message": "Alias de correo redirixido" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "Xerar un alias de correo cun servizo de redirecionado externo." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Dominio de correo", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Seleccionar un dominio compatible co servizo seleccionado", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Erro de $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2990,11 +2912,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Xerado por Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Web: $WEBSITE$. Xerada por Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -3004,7 +2926,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Token da API de $SERVICENAME$ inválido", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3014,7 +2936,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Token da API de $SERVICENAME$ inválido: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3028,7 +2950,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Non foi posible obter o ID de correo enmascarado de $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3038,7 +2960,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Dominio de $SERVICENAME$ inválido.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3048,7 +2970,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Url de $SERVICENAME$ inválido.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -3058,7 +2980,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Ocorreu un erro de $SERVICENAME$ descoñecido.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3068,7 +2990,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Redireccionador descoñecido: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3088,19 +3010,19 @@ "message": "Clave da API" }, "ssoKeyConnectorError": { - "message": "Key connector error: make sure key connector is available and working correctly." + "message": "Erro de conector de clave: asegúrate de que o conector de clave está dispoñible e funcionando correctamente." }, "premiumSubcriptionRequired": { - "message": "Premium subscription required" + "message": "Requírese plan Prémium" }, "organizationIsDisabled": { - "message": "Organization suspended." + "message": "Organización suspendida." }, "disabledOrganizationFilterError": { - "message": "Items in suspended Organizations cannot be accessed. Contact your Organization owner for assistance." + "message": "As entradas de organizacións suspendidas son inaccesibles. Contacta co propietario da organización para asistencia." }, "loggingInTo": { - "message": "Logging in to $DOMAIN$", + "message": "Iniciando sesión en $DOMAIN$", "placeholders": { "domain": { "content": "$1", @@ -3109,25 +3031,25 @@ } }, "settingsEdited": { - "message": "Settings have been edited" + "message": "Os axustes foron modificados" }, "environmentEditedClick": { - "message": "Click here" + "message": "Preme aquí" }, "environmentEditedReset": { - "message": "to reset to pre-configured settings" + "message": "para volver á configuración por defecto" }, "serverVersion": { - "message": "Server version" + "message": "Versión do Servidor" }, "selfHostedServer": { "message": "autoaloxado" }, "thirdParty": { - "message": "Third-party" + "message": "De terceiros" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "Conectado a un servidor dun terceiro, $SERVERNAME$. Por favor verifica bugs empregando o servidor oficial, ou comunícallos ó servidor da terceira parte.", "placeholders": { "servername": { "content": "$1", @@ -3136,7 +3058,7 @@ } }, "lastSeenOn": { - "message": "last seen on: $DATE$", + "message": "Visto por última vez o: $DATE$", "placeholders": { "date": { "content": "$1", @@ -3145,82 +3067,82 @@ } }, "loginWithMasterPassword": { - "message": "Log in with master password" + "message": "Iniciar sesión co contrasinal mestre" }, "loggingInAs": { - "message": "Logging in as" + "message": "Iniciando sesión como" }, "notYou": { - "message": "Not you?" + "message": "Non es ti?" }, "newAroundHere": { - "message": "New around here?" + "message": "Novo por aquí?" }, "rememberEmail": { - "message": "Remember email" + "message": "Lembrar correo" }, "loginWithDevice": { - "message": "Log in with device" + "message": "Iniciar sesión cun dispositivo" }, "loginWithDeviceEnabledInfo": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "O inicio de sesión con dispositivos debe estar activado nos axustes da app de Bitwarden. Precisas doutro método?" }, "fingerprintPhraseHeader": { - "message": "Fingerprint phrase" + "message": "Frase de pegada dixital" }, "fingerprintMatchInfo": { - "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." + "message": "Por favor asegúrate de que a caixa forte está desbloqueada e a frase de pegada dixital coincide ca do outro dispositivo." }, "resendNotification": { - "message": "Resend notification" + "message": "Volver enviar notificación" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Ver todas as opcións de inicio de sesión" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Ver todas as opcións de inicio de sesión" }, "notificationSentDevice": { - "message": "A notification has been sent to your device." + "message": "Enviouse unha notificación ó teu dispositivo." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Enviouse unha notificación ó teu dispositivo" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Por favor asegúrate de que a sesión está aberta e a frase de pegada dixital coincide ca do outro dispositivo" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Serás notificado unha vez se aprobe a solicitude" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Precisas doutro método?" }, "loginInitiated": { - "message": "Login initiated" + "message": "Inicio de sesión comezado" }, "exposedMasterPassword": { - "message": "Exposed Master Password" + "message": "Contrasinal mestre filtrado" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "Contrasinal atopado nunha filtración de datos. Emprega un contrasinal único para a túa conta. Seguro que queres utilizar un contrasinal filtrado?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Contrasinal mestre filtrado e feble" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" + "message": "Contrasinal feble e atopado nunha filtración de datos. Emprega un contrasinal forte e único para a túa conta. Seguro que queres utilizar este contrasinal?" }, "checkForBreaches": { - "message": "Check known data breaches for this password" + "message": "Comprobar este contrasinal en filtracións de datos coñecidas" }, "important": { - "message": "Important:" + "message": "Importante:" }, "masterPasswordHint": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "O contrasinal mestre non pode ser recuperado se o esqueces!" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "Mínimo $LENGTH$ caracteres", "placeholders": { "length": { "content": "$1", @@ -3229,13 +3151,13 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on autofill on page load." + "message": "As directivas da organización activaron o autoenchido ó cargar a páxina." }, "howToAutofill": { - "message": "How to autofill" + "message": "Como autoencher" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "Selecciona unha entrada desta pantalla, emprega o atallo $COMMAND$, ou explora outras opcións en axustes.", "placeholders": { "command": { "content": "$1", @@ -3244,31 +3166,31 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "Selecciona unha entrada desta pantalla ou explora outras opcións en axustes." }, "gotIt": { - "message": "Got it" + "message": "Entendido" }, "autofillSettings": { - "message": "Autofill settings" + "message": "Axustes de autoenchido" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "Atallo de autoenchido" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "Mudar atallo" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "Xestionar atallos" }, "autofillShortcut": { - "message": "Autofill keyboard shortcut" + "message": "Atallo de teclado de autoenchido" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "O atallo de autoenchido de credenciais non está configurado. Cámbiao nos axustes do navegador." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "O atallo de autoenchido de credenciais é $COMMAND$. Xestiona os atallos nos axustes do navegador.", "placeholders": { "command": { "content": "$1", @@ -3277,7 +3199,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default autofill shortcut: $COMMAND$.", + "message": "Atallo de autoenchido por defecto: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -3286,65 +3208,65 @@ } }, "opensInANewWindow": { - "message": "Opens in a new window" + "message": "Abrir nunha ventá nova" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Lembrar este dispositivo para futuros inicios de sesión imperceptibles" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Aprobación de dispositivo requirida. Selecciona unha das seguintes opcións:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Aprobación de dispositivo requirida" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Selecciona unha das seguintes opcións de aprobado" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Lembrar este dispositivo" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Desmarcar se se emprega un dispositivo público" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Aprobar dende o teu outro dispositivo" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Solicitar aprobación do administrador" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Aprobar con contrasinal mestre" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Identificador SSO da organización requirido." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Creando conta en" }, "checkYourEmail": { - "message": "Check your email" + "message": "Revisa o teu correo" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Segue o enlace no correo enviado a" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "e continúa ca creación da conta." }, "noEmail": { - "message": "No email?" + "message": "Sen correo?" }, "goBack": { - "message": "Go back" + "message": "Volver atrás" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "para modificar o teu enderezo electrónico." }, "eu": { "message": "UE", "description": "European Union" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "Acceso denegado. Non tes permiso para ver esta páxina." }, "general": { "message": "Xeral" @@ -3353,51 +3275,51 @@ "message": "Amosar" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Conta creada con éxito!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Aprobación do administrador solicitada" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "A solicitude foi enviada ó teu administrador." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Serás notificado cando se aprobe." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Problemas ao iniciar sesión?" }, "loginApproved": { - "message": "Login approved" + "message": "Inicio de sesión aprobado" }, "userEmailMissing": { - "message": "User email missing" + "message": "Falta o correo electrónico" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Correo electrónico usuario activo non atopado. Cerrando a sesión." }, "deviceTrusted": { - "message": "Device trusted" + "message": "Dispositivo de confianza" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "Sen Sends activos", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Usar send para compartir información cifrada con quen queiras.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { - "message": "Input is required." + "message": "Requírese algunha entrada." }, "required": { - "message": "required" + "message": "requirido" }, "search": { - "message": "Search" + "message": "Busca" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "A entrada debe ser de polo menos $COUNT$ caracteres.", "placeholders": { "count": { "content": "$1", @@ -3406,7 +3328,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "A entrada debe ser de máximo $COUNT$ caracteres.", "placeholders": { "count": { "content": "$1", @@ -3415,7 +3337,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Os seguintes caracteres non están permitidos: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -3424,7 +3346,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "O valor de entrada debe ser de mínimo $MIN$.", "placeholders": { "min": { "content": "$1", @@ -3433,7 +3355,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "O valor de entrada nun debe exceder $MAX$.", "placeholders": { "max": { "content": "$1", @@ -3442,17 +3364,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 ou máis correos son inválidos" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "A entrada non debe conter só espazos en branco.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "A entrada non é un enderezo de correo." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ campo(s) máis arriba precisan revisión.", "placeholders": { "count": { "content": "$1", @@ -3461,10 +3383,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "Un campo precisa revisión." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ campos precisan revisión.", "placeholders": { "count": { "content": "$1", @@ -3473,22 +3395,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Seleccionar --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Escribir para filtrar --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Cargando opcións..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Non se atoparon entradas" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Quitar seleccións" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ máis", "placeholders": { "quantity": { "content": "$1", @@ -3497,176 +3419,144 @@ } }, "submenu": { - "message": "Submenu" + "message": "Submenú" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Colapsar/Expandir", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { - "message": "Alias domain" + "message": "Alias do dominio" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "message": "As entradas que requiran volver a inserir o contrasinal mestre non poden ser autoenchidas ó cargar a páxina.", "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Autofill on page load set to use default setting.", + "message": "Axuste de autoenchido ó cargar a páxina por defecto.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Turn off master password re-prompt to edit this field", + "message": "Desactiva volver a requirir o contrasinal mestre para modificar este campo", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Activar/desactivar navegación lateral" }, "skipToContent": { - "message": "Skip to content" + "message": "Ir ó contido" }, "bitwardenOverlayButton": { - "message": "Bitwarden autofill menu button", + "message": "Botón do menú de autoenchido", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden autofill menu", + "message": "Des/Activar o menú de autoenchido", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "Menú de autoenchido", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "Abre a caixa forte para ver as credenciais coincidentes", "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "Abre a caixa forte para ver as suxestións de autoenchido", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { - "message": "Unlock account", + "message": "Abrir caixa forte", "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "Abrir caixa forte nunha nova ventá", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "Código de verificación basado en tempo de un uso", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "Tempo para que o TOTP actual venza", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { - "message": "Fill credentials for", + "message": "Encher credenciais para", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { - "message": "Partial username", + "message": "Nome de usuario parcial", "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "Sen entradas que amosar", "description": "Text to show in overlay if there are no matching items" }, "newItem": { - "message": "New item", + "message": "Nova entrada", "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { - "message": "Add new vault item", + "message": "Engadir unha nova entrada", "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Nova credencial", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "Engadir unha nova credencial nunha nova ventá", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "Nova tarxeta", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "Engadir unha nova tarxeta nunha nova ventá", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "Nova identidade", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "Engadir unha nova identidade nunha nova ventá", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden autofill menu available. Press the down arrow key to select.", + "message": "Menú de autoenchido dispoñible. Pulsa a tecla de frecha abaixo para seleccionar.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "Activar" }, "ignore": { - "message": "Ignore" + "message": "Ignorar" }, "importData": { - "message": "Import data", + "message": "Importar datos", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Erro ó importar" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Houbo un problema cos datos que intentas importar. Por favor, corrixe os erros listados a continuación e volve intentalo." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Corrixe os erros listados a continuación e volve intentalo." }, "description": { - "message": "Description" + "message": "Descrición" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Datos importados con éxito" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Importáronse $AMOUNT$ entradas.", "placeholders": { "amount": { "content": "$1", @@ -3675,46 +3565,46 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Tentar de novo" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Verificación requirida para esta acción. Crea un PIN para continuar." }, "setPin": { - "message": "Set PIN" + "message": "Crear PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificar con biometría" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Agardando confirmación" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Non se puido completar a biometría." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Precisas dun método alternativo?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar contrasinal mestre" }, "usePin": { - "message": "Use PIN" + "message": "Usar PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biometría" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Insire o código de verificación enviado ó teu correo." }, "resendCode": { - "message": "Resend code" + "message": "Volver enviar o código" }, "total": { "message": "Total" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Estás importando datos a $ORGANIZATION$. Estes datos poden ser compartidos con membros da organización. Queres continuar?", "placeholders": { "organization": { "content": "$1", @@ -3723,49 +3613,49 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "Erro conectando co servizo de Duo. Usa un método de verificación en 2 pasos alternativo ou contacta con Duo." }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Executa Duo e segue os pasos para finalizar o inicio de sesión." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A túa conta require a verificación en 2 pasos de Duo." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Saca a extensión nunha ventá para continuar." }, "popoutExtension": { - "message": "Popout extension" + "message": "Sacar a extensión" }, "launchDuo": { - "message": "Launch Duo" + "message": "Executar Duo" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Datos non estruturados correctamente. Por favor comproba o arquivo e téntao de novo." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Nada foi importado." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Erro descifrando o arquivo exportado. A túa clave de cifrado non coincide ca utilizada ó exportar os datos." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Contrasinal do arquivo inválido. Emprega o contrasinal que creaches cando se exportou o arquivo." }, "destination": { - "message": "Destination" + "message": "Destino" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Aprende acerca das opcións de importado" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Seleccionar un cartafol" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Seleccionar unha colección" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Selecciona esta opción se queres que o contido importado se mova a $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3775,25 +3665,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "O arquivo contén entradas sen asignar." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Selecciona o formato do arquivo de importado" }, "selectImportFile": { - "message": "Select the import file" + "message": "Selecciona o arquivo de importado" }, "chooseFile": { - "message": "Choose File" + "message": "Seleccionar arquivo" }, "noFileChosen": { - "message": "No file chosen" + "message": "Ningún arquivo escollido" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "ou copia/pega o contido do arquivo de importado" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "Instrucións para $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3803,200 +3693,200 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Confirmar o importado da caixa forte" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Este arquivo está protexido. Insire o contrasinal para importar os datos." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Repetir contrasinal do arquivo" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Caixa forte exportada" }, "typePasskey": { - "message": "Passkey" + "message": "Clave de acceso" }, "accessing": { - "message": "Accessing" + "message": "Accedendo" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Sesión iniciada!" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "A Clave de acceso non se vai copiar" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "A Clave de acceso non se incluirá na entrada clonada. Queres continuar co duplicado?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "Verificación requirida polo sitio inicial. Esta función aínda non está implementada para contas sen contrasinal mestre." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "Iniciar sesión con Clave de acceso?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "Xa existe unha Clave de acceso para esta aplicación." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "Non se atoparon Claves de acceso para esta aplicación." }, "noMatchingPasskeyLogin": { - "message": "You do not have a matching login for this site." + "message": "Non tes Claves de acceso coincidentes para este sitio." }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "Non tes credenciais coincidentes para este sitio" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Busca ou garda a Clave de acceso como nova credencial" }, "confirm": { - "message": "Confirm" + "message": "Confirmar" }, "savePasskey": { - "message": "Save passkey" + "message": "Gardar Clave de acceso" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Gardar Clave de acceso como nova credencial" }, "chooseCipherForPasskeySave": { - "message": "Choose a login to save this passkey to" + "message": "Escolle unha credencial na que gardar esta Clave de acceso" }, "chooseCipherForPasskeyAuth": { - "message": "Choose a passkey to log in with" + "message": "Escolle unha Clave de acceso ca que iniciar sesión" }, "passkeyItem": { - "message": "Passkey Item" + "message": "Entrada de Clave de acceso" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "Sobrescribir Clave de acceso?" }, "overwritePasskeyAlert": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "Esta entrada xa contén unha Clave de acceso. Seguro que queres sobreescribir a existente?" }, "featureNotSupported": { - "message": "Feature not yet supported" + "message": "Función aínda non implementada" }, "yourPasskeyIsLocked": { - "message": "Authentication required to use passkey. Verify your identity to continue." + "message": "A autenticación require unha Clave de acceso. Verifica a túa identidade para continuar." }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Autenticación multifactor cancelada" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Non se atoparon datos de LastPass" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Nome de usuario ou contrasinal incorrectos" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Contrasinal incorrecto" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Código incorrecto" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN incorrecto" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Autenticación multifactor fallida" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Incluír cartafois compartidos" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "Correo de LastPass" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Importar a túa conta..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Autenticación multifactor de LastPass requirida" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Insire o código de un uso da túa app de autenticación" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Aproba a petición de inicio de sesión na túa app de autenticación ou insire o código dun uso." }, "passcode": { - "message": "Passcode" + "message": "Código de acceso" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "Contrasinal mestre de LastPass" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Autenticación de LastPass requirida" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Agardando pola autenticación SSO" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Por favor, continúa empregando as credenciais da túa compañía." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Ver instrucións detalladas no noso sitio de axuda en", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Importar directamente de LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Importar dun CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Volver intentar ou buscar un correo de LastPass para verificar que es ti." }, "collection": { - "message": "Collection" + "message": "Colección" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Conecta a YubiKey asociada á túa conta de LastPass, e preme o seu botón." }, "switchAccount": { - "message": "Switch account" + "message": "Cambiar de conta" }, "switchAccounts": { - "message": "Switch accounts" + "message": "Cambiar as contas" }, "switchToAccount": { - "message": "Switch to account" + "message": "Cambiar a conta" }, "activeAccount": { - "message": "Active account" + "message": "Activar conta" }, "availableAccounts": { - "message": "Available accounts" + "message": "Contas dispoñibles" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "Límite de contas alcanzado. Cerra sesión nunha delas para engadir outra." }, "active": { - "message": "active" + "message": "activo/a" }, "locked": { - "message": "locked" + "message": "bloqueado" }, "unlocked": { - "message": "unlocked" + "message": "desbloqueado" }, "server": { - "message": "server" + "message": "servidor" }, "hostedAt": { - "message": "hosted at" + "message": "aloxado en" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "Usa o teu dispositivo ou chave de hardware" }, "justOnce": { - "message": "Just once" + "message": "Só unha vez" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "Sempre para este sitio" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "$DOMAIN$ engadido ós dominios excluídos.", "placeholders": { "domain": { "content": "$1", @@ -4005,103 +3895,106 @@ } }, "commonImportFormats": { - "message": "Common formats", + "message": "Formatos comúns", "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Ir ós axustes do navegador?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "Ir ó Centro de Axuda?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "Cambiar os axustes de autoenchido e xestión de contrasinais do navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Podes ver e crear atallos da extensión nos axustes do teu navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "Cambiar os axustes de autoenchido e xestión de contrasinais do navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Podes ver e crear atallos da extensión nos axustes do teu navegador.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Facer de Bitwarden o teu xestor de contrasinais por defecto?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "Ignorar esta opción pode causar conflitos entre Bitwarden e o xestor de contrasinais do teu navegador.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Facer de Bitwarden o teu xestor de contrasinais por defecto", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Non se puido facer de Bitwarden o xestor de contrasinais por defecto", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "Debes conceder ó navegador permisos de privacidade sobre Bitwarden para facelo o xestor de contrasinais por defecto.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Facer o predefinido", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Credenciais gardadas con éxito!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Contrasinal gardado!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Credenciais actualizadas con éxito!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Contrasinal actualizado!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Erro ó gardar as credenciais. Comproba a consola para máis detalle.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Éxito" }, "removePasskey": { - "message": "Remove passkey" + "message": "Eliminar Clave de acceso" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Clave de acceso eliminada" }, "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Entradas suxeridas" + }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "Gardar unha credencial como suxestión para este sitio" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "A caixa forte está baleira" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "Non hai entradas que coincidan ca túa busca" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "Quitar filtros e tentar outro termo de busca" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Copiar información - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4111,7 +4004,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "Copiar nota - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4121,7 +4014,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Máis opcións, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4131,7 +4024,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Máis opcións - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4141,7 +4034,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Ver entrada - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4151,7 +4044,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "Autoenchido - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4161,40 +4054,40 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Non hai valores que copiar" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Vincular con coleccións" }, "copyEmail": { - "message": "Copy email" + "message": "Copiar correo" }, "copyPhone": { - "message": "Copy phone" + "message": "Copiar teléfono" }, "copyAddress": { - "message": "Copy address" + "message": "Copiar enderezo" }, "adminConsole": { - "message": "Admin Console" + "message": "Consola do administrador" }, "accountSecurity": { - "message": "Account security" + "message": "Seguridade da conta" }, "notifications": { - "message": "Notifications" + "message": "Notificacións" }, "appearance": { - "message": "Appearance" + "message": "Aparencia" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Erro ó vincular esta colección." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Erro ó vincular este cartafol." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Ver entradas en $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4204,7 +4097,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Volver a $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4214,10 +4107,10 @@ } }, "new": { - "message": "New" + "message": "Novo" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Eliminar $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4227,16 +4120,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Entradas sen carpeta" }, "itemDetails": { - "message": "Item details" + "message": "Detalles do entrada" }, "itemName": { - "message": "Item name" + "message": "Nome da entrada" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Non podes eliminar coleccións con permisos de Só lectura: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -4245,47 +4138,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "A organización está desactivada" }, "owner": { - "message": "Owner" + "message": "Propietario" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ti", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "As entradas en organizacións suspendidas son inaccesibles. Contacta co propietario da organización para asistencia." }, "additionalInformation": { - "message": "Additional information" + "message": "Información adicional" }, "itemHistory": { - "message": "Item history" + "message": "Historial da entrada" }, "lastEdited": { - "message": "Last edited" + "message": "Modificado o" }, "ownerYou": { - "message": "Owner: You" + "message": "Propietario: Ti" }, "linked": { - "message": "Linked" + "message": "Vinculado" }, "copySuccessful": { - "message": "Copy Successful" + "message": "Copiado realizado" }, "upload": { - "message": "Upload" + "message": "Subir" }, "addAttachment": { - "message": "Add attachment" + "message": "Anexar arquivo" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "O tamaño máximo é de 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Eliminar anexos $NAME$", "placeholders": { "name": { "content": "$1", @@ -4294,7 +4187,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Descargar $NAME$", "placeholders": { "name": { "content": "$1", @@ -4303,25 +4196,25 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Estás seguro de que queres eliminar permanentemente este anexo?" }, "premium": { - "message": "Premium" + "message": "Prémium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "As organizacións gratuitas non poden empregar anexos" }, "filters": { - "message": "Filters" + "message": "Filtros" }, "filterVault": { - "message": "Filter vault" + "message": "Filtros da caixa forte" }, "filterApplied": { - "message": "One filter applied" + "message": "Un filtro en uso" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ filtros en uso", "placeholders": { "count": { "content": "$1", @@ -4330,16 +4223,16 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "Detalles persoais" }, "identification": { - "message": "Identification" + "message": "Identificación" }, "contactInfo": { - "message": "Contact info" + "message": "Información de contacto" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "Descargar - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4348,23 +4241,23 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "o número de tarxeta remata en", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "Credenciais de inicio de sesión" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Clave de autenticación" }, "autofillOptions": { - "message": "Autofill options" + "message": "Opcións de autoenchido" }, "websiteUri": { - "message": "Website (URI)" + "message": "Dirección web (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "Dirección web (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4374,16 +4267,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Dirección web engadida" }, "addWebsite": { - "message": "Add website" + "message": "Engadir dirección web" }, "deleteWebsite": { - "message": "Delete website" + "message": "Eliminar dirección web" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Predeterminado ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4393,7 +4286,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "Mostrar detección de coincidencia $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4402,7 +4295,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "Agochar detección de coincidencia $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4411,19 +4304,19 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "Autoencher ó cargar a páxina?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Tarxeta vencida" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "Se a renovas, actualiza a información da tarxeta" }, "cardDetails": { - "message": "Card details" + "message": "Detalles da tarxeta" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "Detalles da $BRAND$", "placeholders": { "brand": { "content": "$1", @@ -4432,43 +4325,43 @@ } }, "enableAnimations": { - "message": "Enable animations" + "message": "Activar animacións" }, "showAnimations": { - "message": "Show animations" + "message": "Amosar animacións" }, "addAccount": { - "message": "Add account" + "message": "Engadir conta" }, "loading": { - "message": "Loading" + "message": "Cargando" }, "data": { - "message": "Data" + "message": "Datos" }, "passkeys": { - "message": "Passkeys", + "message": "Claves de acceso", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Contrasinais", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "Iniciar sesión con Clave de acceso", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "Asignar" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "Só os membros da organización con acceso a estas coleccións poderán ver esta entrada." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "Só os membros da organización con acceso a estas coleccións poderán ver estas entradas." }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Seleccionaches $TOTAL_COUNT$ entradas. Non vas poder modificar $READONLY_COUNT$ delas porque non tes permisos de edición.", "placeholders": { "total_count": { "content": "$1", @@ -4480,37 +4373,37 @@ } }, "addField": { - "message": "Add field" + "message": "Engadir un campo" }, "add": { - "message": "Add" + "message": "Engadir" }, "fieldType": { - "message": "Field type" + "message": "Tipo de campo" }, "fieldLabel": { - "message": "Field label" + "message": "Título do campo" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "Emprega campos de texto para datos como preguntas de seguridade" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "Emprega campos agochados para datos sensibles como contrasinais" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "Emprega caixas de verificación cando queiras autoencher as dun formulario, como \"Lembrar correo\"" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "Emprega un campo vinculado cando teñas problemas coa autoenchido dalgunha web." }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "Insire o ID HTML, nome, aria-label ou exemplo foi campo." }, "editField": { - "message": "Edit field" + "message": "Modificar campo" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "Modificar $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4519,7 +4412,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "Eliminar $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4528,7 +4421,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ engadido", "placeholders": { "label": { "content": "$1", @@ -4537,7 +4430,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "Recolocar $LABEL$. Emprega as frechas do teclado.", "placeholders": { "label": { "content": "$1", @@ -4546,7 +4439,7 @@ } }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "Subiuse $LABEL$ á posición $INDEX$ de $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4563,13 +4456,13 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Seleccionar coleccións a vincular" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "Unha entrada será irreversiblemente transferida á organización. Xa non serás o seu propietario." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ entradas serán irreversiblemente transferidas á organización. Xa non serás o seu propietario.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4578,7 +4471,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "Unha entrada será irreversiblemente transferida a $ORG$. Xa non serás o seu propietario.", "placeholders": { "org": { "content": "$1", @@ -4587,7 +4480,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ entradas serán irreversiblemente transferidas a $ORG$. Xa non serás o seu propietario.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4600,13 +4493,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Coleccións vinculadas" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "Non tes nada seleccionado." }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "Entradas seleccionadas transferidas a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4615,7 +4508,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "Entradas transferidas a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4624,7 +4517,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "Entrada transferida a $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4633,7 +4526,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "Baixouse $LABEL$ á posición $INDEX$ de $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4650,52 +4543,46 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "Ubicación da entrada" }, "fileSend": { - "message": "File Send" + "message": "Arquivo Send" }, "fileSends": { - "message": "File Sends" + "message": "Arquivos Send" }, "textSend": { - "message": "Text Send" + "message": "Texto Send" }, "textSends": { - "message": "Text Sends" - }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + "message": "Textos Send" }, "accountActions": { - "message": "Account actions" + "message": "Accións da conta" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "Amosar o número de suxestións de credenciais na icona da extensión" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Amosar accións rápidas de copiado na caixa forte" }, "systemDefault": { - "message": "System default" + "message": "Predefinido do sistema" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "Directivas da empresa foron aplicadas a esta opción" }, "sshPrivateKey": { - "message": "Private key" + "message": "Clave privada" }, "sshPublicKey": { - "message": "Public key" + "message": "Clave pública" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Pegada dixital" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipo de clave" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4710,223 +4597,250 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "Reintentar" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "O tempo mínimo personalizado é de 1 minuto." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "Hai dispoñibles contidos adicionais" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "Arquivo gardado no dispositivo. Xestiónao dende as descargas do dispositivo." }, "showCharacterCount": { - "message": "Show character count" + "message": "Amosar contador de caracteres" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "Ocultar contador de caracteres" }, "itemsInTrash": { - "message": "Items in trash" + "message": "Entradas no lixo" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "Sen entradas no lixo" }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "As entradas que elimines aparecerán aquí serán permanente eliminadas despois de 30 días" }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "As entradas que estean no lixo máis de 30 días serán automaticamente eliminadas" }, "restore": { - "message": "Restore" + "message": "Restaurar" }, "deleteForever": { - "message": "Delete forever" + "message": "Eliminar permanentemente" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "Non tes permiso para modificar esta entrada" + }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "O desbloqueo biométrico non está dispoñible porque se require o PIN ou contrasinal primeiro." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "O desbloqueo biométrico non está dispoñible." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "O desbloqueo biométrico non está dispoñible por arquivos do sistema desconfigurados." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "O desbloqueo biométrico non está dispoñible por arquivos do sistema desconfigurados." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "O desbloqueo biométrico non está dispoñible porque a aplicación de escritorio está pechada." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "O desbloqueo biométrico non está dispoñible porque non está activada para $EMAIL$ na aplicación de escritorio.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "O desbloqueo biométrico non está dispoñible por algunha razón non prevista." }, "authenticating": { - "message": "Authenticating" + "message": "Autenticando" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "Encher co contrasinal xerado", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Contrasinal xerado de novo", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Gardar credenciais en Bitwarden?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Espazo", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "Til", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Plica", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Exclamación", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Arroba", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "Símbolo de almohadilla", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Símbolo do dólar", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Símbolo de porcentaxe", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "Símbolo circunflexo", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Símbolo E comercial", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Símbolo Asterisco", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Símbolo abrir paréntese", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Símbolo pechar paréntese", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Símbolo guión baixo", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Símbolo guión", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Símbolo máis", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "Símbolo igual", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Símbolo abrir chave", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Símbolo pechar chave", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Símbolo abrir corchete", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Símbolo pechar corchete", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "Símbolo de liña vertical (pipe)", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Símbolo de barra invertida", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Símbolo de dous puntos", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Símbolo de punto e coma", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Símbolo de dobre comilla", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Símbolo de comilla simple", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Símbolo de menor que", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Símbolo de maior que", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Símbolo de coma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Símbolo de punto", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Símbolo de interrogación", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Símbolo de barra inclinada", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Minúsculas" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Maiúsculas" }, "generatedPassword": { - "message": "Generated password" + "message": "Contrasinal xerado" }, "compactMode": { - "message": "Compact mode" + "message": "Modo compacto" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Aviso importante" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Configurar verificación en dous pasos" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "A partir de febreiro de 2025 Bitwarden comezará a enviar correos con códigos de verificación para confirmar novos inicios de sesión á túa conta." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Podes configurar a verificación en 2 pasos como alternativa para protexer a túa conta ou cambiar o enderezo electrónico a un ó que teñas acceso." }, "remindMeLater": { - "message": "Remind me later" + "message": "Lembrarmo máis tarde" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Tes acceso fiable ó teu correo? ($EMAIL$)", "placeholders": { "email": { "content": "$1", @@ -4935,24 +4849,24 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Non, non o teño" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Si, teño acceso fiable ó meu correo" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Activar verificación en dous pasos" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Mudar de correo electrónico" }, "extensionWidth": { - "message": "Extension width" + "message": "Ancho da extensión" }, "wide": { - "message": "Wide" + "message": "Ancho" }, "extraWide": { - "message": "Extra wide" + "message": "Moi ancho" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index a069afdc69a..b66d0a4aa24 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -454,9 +454,6 @@ "length": { "message": "אורך" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "מינימום תוים מיוחדים" }, - "avoidAmbChar": { - "message": "המנע מאותיות ותוים דומים", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "נקה לוח העתקות", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "שתף" - }, "movedItemToOrg": { "message": "$ITEMNAME$ הועבר ל- $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "שכפול" }, - "passwordGeneratorPolicyInEffect": { - "message": "מדיניות ארגונית אחת או יותר משפיעה על הגדרות המחולל שלך." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "הקובץ שברצונך לשלוח." - }, "deletionDate": { "message": "תאריך מחיקה" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "תאריך תפוגה" }, - "expirationDateDesc": { - "message": "במידה ויוגדר, הגישה ל Send זה תושבת בתאריך ובשעה שהוגדרו.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "יום אחד" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "מותאם אישית" }, - "maximumAccessCount": { - "message": "כמות גישות מרבית" - }, - "maximumAccessCountDesc": { - "message": "במידה ויוגדר, משתמשים לא יוכלו יותר לגשת ל Send זה לאחר שמספר הגישות המרבי יושג.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "שגיאה" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "סוג שם משתמש" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4086,7 +3976,10 @@ "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "הצעות למילוי אוטומטי" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 288aa1171ce..8927c78e16b 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -20,16 +20,16 @@ "message": "Create Account" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "बिटवार्डन का परिचय" }, "logInWithPasskey": { "message": "Log in with passkey" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "सिंगल साइन-ऑन प्रयोग करें" }, "welcomeBack": { - "message": "Welcome back" + "message": "आपका पुन: स्वागत है!" }, "setAStrongPassword": { "message": "मजबूत पासवर्ड सेट करें" @@ -138,10 +138,10 @@ "message": "Copy Security Code" }, "copyName": { - "message": "Copy name" + "message": "नाम कॉपी करें" }, "copyCompany": { - "message": "Copy company" + "message": "कंपनी के नाम को कॉपी करें" }, "copySSN": { "message": "Copy Social Security number" @@ -454,9 +454,6 @@ "length": { "message": "लंबाई" }, - "passwordMinLength": { - "message": "न्यूनतम पासवर्ड लंबाई" - }, "uppercase": { "message": "बड़े अक्षर (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum Special" }, - "avoidAmbChar": { - "message": "Avoid Ambiguous Characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "आसान ऑटो-फिल के लिए टैब पेज पर कार्ड आइटम सूचीबद्ध करें।" }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "क्लिपबोर्ड खाली करें", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "संगठन में ले जाएँ" }, - "share": { - "message": "शेयर करें" - }, "movedItemToOrg": { "message": "$ITEMNAME$ $ORGNAME$ गया ", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "क्लोन" }, - "passwordGeneratorPolicyInEffect": { - "message": "एक या एक से अधिक संगठन नीतियां आपकी जनरेटर सेटिंग को प्रभावित कर रही हैं।" - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "बहिष्कृत डोमेन" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Sends मे खोजे", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Send जोड़ें", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "शब्द" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "मैक्स एक्सेस काउंट पहुंच गया है", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": " गतावधिक" }, - "pendingDeletion": { - "message": "हटाना लंबित" - }, "passwordProtected": { "message": "पासवर्ड सुरक्षित है" }, @@ -2472,24 +2462,9 @@ "message": "एडिट Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "यह किस प्रकार का सेंड है?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "इस सेंड का वर्णन करने के लिए एक दोस्ताना नाम।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "वह फाइल जो आप सेंड करना चाहते हैं।" - }, "deletionDate": { "message": "हटाने की तारीख" }, - "deletionDateDesc": { - "message": " यह सेंड निर्धारित तिथि और समय पर स्थायी रूप से हटा दिया जाएगा।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "समाप्ति तिथि" }, - "expirationDateDesc": { - "message": "यदि सेट किया जाता है, तो यह सेंड निर्दिष्ट तिथि और समय पर समाप्त हो जाएगा।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 दिन" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "कस्टम" }, - "maximumAccessCount": { - "message": "अधिकतम एक्सेस काउंट" - }, - "maximumAccessCountDesc": { - "message": "यदि सेट किया जाता है, तो अधिकतम एक्सेस काउंट तक पहुंचने के बाद उपयोगकर्ता अब इस सेंड को एक्सेस नहीं कर पाएंगे।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "वैकल्पिक रूप से उपयोगकर्ताओं को इस सेंड तक पहुंचने के लिए पासवर्ड की आवश्यकता होगी।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "इस सेंड के बारे में निजी नोट्स।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "इस सेंड को अक्षम करें ताकि कोई भी इसे एक्सेस न कर सके।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "सेव पर क्लिपबोर्ड पर इस सेंड के लिंक को कॉपी करें।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "वह टेक्स्ट जो आप सेंड करना चाहते हैं।" - }, - "sendHideText": { - "message": "इस सेंड के टेक्स्ट को डिफ़ॉल्ट रूप से छिपाएं।", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "वर्तमान एक्सेस काउंट" - }, "createSend": { "message": "नया सेंड बनाएं", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "शुरू करने से पहले" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "कैलेंडर शैली तिथि बीनने वाले का उपयोग करने के लिए", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "यहां क्लिक करें", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "अपनी विंडो पॉप आउट करने के लिए यहां क्लिक करें।", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "प्रदान की गई समाप्ति तिथि मान्य नहीं है।" }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "आपके विलोपन और समाप्ति तिथियों को सहेजने में एक त्रुटि थी।" }, - "hideEmail": { - "message": "प्राप्तकर्ताओं से मेरा ईमेल पता छिपाएं।" - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "एक या एक से अधिक संगठन नीतियां आपके सेंड विकल्पों को प्रभावित कर रही हैं।" - }, "passwordPrompt": { "message": "मास्टर पासवर्ड रि-प्रॉम्प्ट" }, @@ -2884,8 +2804,19 @@ "error": { "message": "एरर" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "उपयोगकर्ता नाम बनाएँ" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "वेबसाइट का नाम" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "डोमेन उपनाम" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "बिटवार्डन का नया रूप!" - }, - "bitwardenNewLookDesc": { - "message": "वॉल्ट टैब से ऑटोफिल और सर्च करना पहले से कहीं ज़्यादा आसान और सहज है। सबकुछ ध्यान से देखें!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4911,10 +4825,10 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "महत्वपूर्ण सूचना" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "टू-स्टेप लॉगइन" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." @@ -4923,7 +4837,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "बाद में मुझे याद कराएं" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -4935,16 +4849,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "नहीं, मैं नहीं" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "हाँ, मैं आराम से अपना ईमेल देख सकता हूँ" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "टू-स्टेप लॉगइन सक्षम करें" }, "changeAcctEmail": { - "message": "Change account email" + "message": "अकाउंट का ईमेल बदलें" }, "extensionWidth": { "message": "Extension width" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index a8c680ac666..20583c4cbbd 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Duljina" }, - "passwordMinLength": { - "message": "Minimalna duljina lozinke" - }, "uppercase": { "message": "Velika slova (A - Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Najmanje posebnih" }, - "avoidAmbChar": { - "message": "Izbjegavaj dvosmislene znakove", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Izbjegavaj dvosmislene znakove", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Prikazuj identitete za jednostavnu auto-ispunu." }, + "clickToAutofillOnVault": { + "message": "Klikni stavke za auto-ispunu na prikazu trezora" + }, "clearClipboard": { "message": "Očisti međuspremnik", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Premjesti u organizaciju" }, - "share": { - "message": "Podijeli" - }, "movedItemToOrg": { "message": "$ITEMNAME$ premješteno u $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Kloniraj" }, - "passwordGeneratorPolicyInEffect": { - "message": "Jedno ili više pravila organizacije utječe na postavke generatora." - }, "passwordGenerator": { "message": "Generator lozinki" }, @@ -2334,6 +2324,9 @@ "message": "Domene", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Izuzete domene" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden neće nuditi spremanje podataka za prijavu za ove domene za sve prijavljene račune. Moraš osvježiti stranicu kako bi promjene stupile na snagu." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Web stranica $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Spremljene promjene izuzete domene" }, @@ -2389,14 +2394,6 @@ "message": "Detalji Senda", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Pretraži Sendove", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Dodaj Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekst" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Zadano sakrij tekst" }, - "maxAccessCountReached": { - "message": "Dostignut najveći broj pristupanja", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Isteklo" }, - "pendingDeletion": { - "message": "Čeka brisanje" - }, "passwordProtected": { "message": "Zaštićeno lozinkom" }, @@ -2472,24 +2462,9 @@ "message": "Uredi Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Koja je ovo vrsta Senda?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Nadimak za ovaj Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Datoteka koju želiš poslati" - }, "deletionDate": { "message": "Obriši za" }, - "deletionDateDesc": { - "message": "Send će nakon navedenog vremena biti trajno izbrisan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send će na ovaj datum biti trajno izbrisan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Vremenski ograničeni pristup" }, - "expirationDateDesc": { - "message": "Pristup ovom Sendu neće biti moguć nakon navednog roka.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dan" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Prilagođeno" }, - "maximumAccessCount": { - "message": "Ograničeni broj pristupanja" - }, - "maximumAccessCountDesc": { - "message": "Ako je određen, ovom Sendu će se moći pristupiti samo ograničeni broj puta.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Neobavezno zahtijevaj korisnika lozinku za pristup ovom Sendu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Dodaj opcionalnu lozinku za primatelje ovog Senda.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Privatne bilješke o Sendu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Onemogući ovaj Send da mu nitko ne može pristupiti.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopiraj vezu na Send nakon spremanja", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Tekst kojeg želiš poslati" - }, - "sendHideText": { - "message": "Sakrij tekst ovog Senda.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Trenutni broj pristupanja" - }, "createSend": { "message": "Stvori novi Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Prije početka" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Biranje datuma na kalendaru", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klikni ovjde", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "za iskočni prozor", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Navedeni rok isteka nije valjan." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Došlo je do greške kod spremanja vaših datuma isteka i brisanja." }, - "hideEmail": { - "message": "Sakrij moju adresu e-pošte od primatelja." - }, "hideYourEmail": { "message": "Autentifikacija" }, - "sendOptionsPolicyInEffect": { - "message": "Jedno ili više pravila organizacije utječe na postavke Senda." - }, "passwordPrompt": { "message": "Ponovno zatraži glavnu lozinku" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Pogreška" }, - "regenerateUsername": { - "message": "Ponovno generiraj korisničko ime" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generiraj korisničko ime" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Tip korisničkog imena" - }, "plusAddressedEmail": { "message": "Plus adresa e-pošte", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Naziv web mjesta" }, - "whatWouldYouLikeToGenerate": { - "message": "Što želiš generirati?" - }, - "passwordType": { - "message": "Tip lozinke" - }, "service": { "message": "Usluga" }, @@ -3503,38 +3425,6 @@ "message": "Sažmi/Proširi", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Uvezi svoje podatke u Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Zaštititi svoje LastPass podatke i uvezi ih u Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Spremi kao nekriptiranu datoteku", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Uvezi u Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Uvoz...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Uvoz podataka u trezor uspješan!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Greška pri uvozu. Provjeri konzolu za detalje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Došlo je do mrežne greške tijekom uvoza.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domene" }, @@ -4086,7 +3976,10 @@ "message": "Pristupni ključ uklonjen" }, "autofillSuggestions": { - "message": "Prijedlozi auto-ispune" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Spremi u auto-ispunu stavku prijave za ovu stranicu" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Send tekstovi" }, - "bitwardenNewLook": { - "message": "Bitwarden ima novi izgled!" - }, - "bitwardenNewLookDesc": { - "message": "Auto-ispuna i pretraga iz kartice Trezor je lakša i intuitivnija nego ikad prije. Razgledaj!" - }, "accountActions": { "message": "Radnje na računu" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Nemaš prava za uređivanje ove stavke" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autentifikacija" }, @@ -4911,22 +4825,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Važna napomena" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Postavi dvostruku autentifikaciju" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden će, počevši od veljače 2025., za provjeru prijava s novih uređaja poslati kôd na e-poštu tvog računa." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Prijavu dvostrukom autentifikacijom možeš postaviti kao alternativni način zaštite svog računa ili promijeni svoju e-poštu u onu kojoj možeš pristupiti." }, "remindMeLater": { - "message": "Remind me later" + "message": "Podsjeti me kasnije" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Imaš li pouzdan pristup svojoj e-pošti: $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4935,16 +4849,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Ne, nemam" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Da, pouzdano mogu pristupiti svojoj e-pošti" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Uključi prijavu dvostrukom autentifikacijom" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Promjeni e-poštu računa" }, "extensionWidth": { "message": "Širina proširenja" diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 07662b83157..bcde4db3c4f 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Hossz" }, - "passwordMinLength": { - "message": "Minimum jelszó hosszúság" - }, "uppercase": { "message": "Nagybetűs (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimális speciális" }, - "avoidAmbChar": { - "message": "Félreérthető karakterek mellőzése", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Félreérthető karakterek mellőzése", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Azonosítás elemek listázása a Fül oldalon a könnyű automatikus kitöltéshez." }, + "clickToAutofillOnVault": { + "message": "Kattintsunk az elemekre az automatikus kitöltéshez a Széf nézetben" + }, "clearClipboard": { "message": "Vágólap ürítése", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Áthelyezés szervezethez" }, - "share": { - "message": "Megosztás" - }, "movedItemToOrg": { "message": "$ITEMNAME$ átkerült $ORGNAME$ szervezethez", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Klónozás" }, - "passwordGeneratorPolicyInEffect": { - "message": "Egy vagy több szervezeti szabály érinti a generátor beállításokat." - }, "passwordGenerator": { "message": "Jelszó generátor" }, @@ -2334,6 +2324,9 @@ "message": "Tartomány", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Letiltott tartományok" + }, "excludedDomains": { "message": "Kizárt domainek" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "A Bitwarden nem kéri a bejelentkezési adatok mentését ezeknél a tartományoknál az összes bejelentkezési fiókra vonatkozva. A változtatások életbe lépéséhez frissíteni kell az oldalt." }, + "blockedDomainsDesc": { + "message": "Az automatikus kitöltés és az egyéb kapcsolódó funkciók ezeken a webhelyeken nincsenek a kínálatban. A változtatások életbe lépéséhez frissíteni kell az oldalt." + }, + "autofillBlockedNoticeV2": { + "message": "Az automatikus kitöltés blokkolásra került ezen a webhelyen." + }, + "autofillBlockedNoticeGuidance": { + "message": "Megváltoztatás a beállításokban" + }, "websiteItemLabel": { "message": "Webhely $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "A letiltott tartomány módosítások mentésre kerültek." + }, "excludedDomainsSavedSuccess": { "message": "A kizárt tartomány módosítások mentésre kerültek." }, @@ -2389,14 +2394,6 @@ "message": "Send részletek", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Send keresése", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Send hozzáadása", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Szöveg" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Szöveg elrejtése alapértelmezetten" }, - "maxAccessCountReached": { - "message": "A maximális hozzáférések száma elérésre került.", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Lejárt" }, - "pendingDeletion": { - "message": "Függőben lévő törlés" - }, "passwordProtected": { "message": "Jelszóval védett" }, @@ -2472,24 +2462,9 @@ "message": "Send szerkesztése", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Milyen típusú ez a Send?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Barátságos név a Send leírására.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "A küldendő fájl." - }, "deletionDate": { "message": "Törlési dátum" }, - "deletionDateDesc": { - "message": "A Send véglegesen törölve lesz a meghatározott időpontban.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "A Send véglegesen törölve lesz ebben az időpontban.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Lejárati dátum" }, - "expirationDateDesc": { - "message": "Amennyiben be van állítva, a hozzáférés ehhez a Sendhez a meghatározott időpontban lejár", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 nap" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Egyedi" }, - "maximumAccessCount": { - "message": "Maximális elérési szám" - }, - "maximumAccessCountDesc": { - "message": "Beállítva a Küldés elérhetetlen lesz a meghatározott hozzáférések számának elérése után.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Opcionálisan megadhatunk egy jelszót a felhasználók számára a Küldés eléréséhez. ", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Adjunk meg egy opcionális jelszót a címzetteknek a Send eléréséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Személyes megjegyzések erről a Küldésről.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "A Send letiltásával senki nem férhet hozzá.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Mentéskor másoljuk a Küldés hivatkozását a vágólapra.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "A küldendő fájl." - }, - "sendHideText": { - "message": "Alapértelmezés szerint elrejti a Küldés szövegét.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Aktuális elérési szám" - }, "createSend": { "message": "Új Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Mielőtt belevágnánk" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Naptár-stílusú dátumválasztáshoz", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "kattintás ide", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "az ablak megnyitásához", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "A megadott lejárati idő nem érvényes." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Hiba történt a törlési és a lejárati dátum mentésekor." }, - "hideEmail": { - "message": "Saját email cím elrejtése a címzettek elől." - }, "hideYourEmail": { "message": "Saját email cím elrejtése a megtekintések elől." }, - "sendOptionsPolicyInEffect": { - "message": "Egy vagy több szervezeti szabály érinti a Send opciókat." - }, "passwordPrompt": { "message": "Mesterjelszó ismételt megadás" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Hiba" }, - "regenerateUsername": { - "message": "Felhasználónév ismételt geneálása" + "decryptionError": { + "message": "Visszafejtési hiba" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "A Bitwarden nem tudta visszafejteni az alább felsorolt ​​széf elemeket." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Ügyfélszolgálat elérése", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "további adatvesztés elkerülése érdekében.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Felhasználónév generálása" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Felhasználónév típusa" - }, "plusAddressedEmail": { "message": "További címzési email cím", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Webhelynév" }, - "whatWouldYouLikeToGenerate": { - "message": "Mit szeretnénk generálni?" - }, - "passwordType": { - "message": "Jelszótípus" - }, "service": { "message": "Szolgáltatás" }, @@ -3503,38 +3425,6 @@ "message": "Összezárás váltás", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Adatok importálása a Bitwardenbe?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "A LastPass adatok megvédése és importálása a Bitwardenbe?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Mentés titkosítatlan fájlként", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importálás a Bitwardenbe", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importálás...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Az adatok sikeresen importálásra kerültek.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Hiba történt az importálás során. A részletekért ellenőrizzük a konzolt.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Hálózati hiba történt az importálás során.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Áldomain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Automatikus kitöltés javaslatok" }, + "itemSuggestions": { + "message": "Javasolt elemek" + }, "autofillSuggestionsTip": { "message": "A bejelentkezési elem mentése ehhez a webhelyhez az automatikus kitöltéshez" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Szöveg küldés" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Fiókműveletek" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Nincs jogosulltság ezen elem szerkesztéséheu." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "A biometrikus feloldás nem érhető el, mert először PIN kóddal vagy jelszóval kell feloldani." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "A biometrikus feloldás jelenleg nem érhető el." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "A biometrikus feloldás nem érhető el, mert a Bitwarden asztali alkalmazás be van zárva." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "A biometrikus feloldás nem érhető el, mert nincs engedélyezve $EMAIL$ számára a Bitwarden asztali alkalmazásban.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "A biometrikus feloldás jelenleg ismeretlen okból nem érhető el." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index ef8e1431284..fedfb0cec1f 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Panjang" }, - "passwordMinLength": { - "message": "Panjang kata sandi minimum" - }, "uppercase": { "message": "Huruf besar (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Spesial Minimum" }, - "avoidAmbChar": { - "message": "Hindari Karakter Ambigu", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Hindari karakter ambigu", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Buat tampilan daftar benda dari identitas pada halaman Tab untuk isi otomatis yang mudah." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Hapus Papan Klip", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Pindah ke Organisasi" }, - "share": { - "message": "Bagikan" - }, "movedItemToOrg": { "message": "$ITEMNAME$ pindah ke $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Duplikat" }, - "passwordGeneratorPolicyInEffect": { - "message": "Satu atau lebih kebijakan organisasi mempengaruhi pengaturan pembuat sandi Anda." - }, "passwordGenerator": { "message": "Pembuat kata sandi" }, @@ -2334,6 +2324,9 @@ "message": "Domain", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domain yang Dikecualikan" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden tidak akan meminta untuk menyimpan rincian login untuk domain tersebut. Anda harus menyegarkan halaman agar perubahan diterapkan." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Situs web $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Perubahan domain yang diabaikan telah disimpan" }, @@ -2389,14 +2394,6 @@ "message": "Rincian Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Pencarian Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Tambahkan Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Teks" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Sembunyikan teks secara bawaan" }, - "maxAccessCountReached": { - "message": "Jumlah akses maksimum tercapai", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Kedaluwarsa" }, - "pendingDeletion": { - "message": "Penghapusan menunggu keputusan" - }, "passwordProtected": { "message": "Dilindungi kata sandi" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Jenis Send apakah ini?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Nama yang bersahabat untuk menggambarkan Send ini.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "File yang ingin Anda kirim." - }, "deletionDate": { "message": "Tanggal Penghapusan" }, - "deletionDateDesc": { - "message": "Send akan dihapus secara permanen pada tanggal dan waktu yang ditentukan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send akan dihapus selamanya pada tanggal ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Tanggal habis tempo" }, - "expirationDateDesc": { - "message": "Jika disetel, akses ke Send ini akan berakhir pada tanggal dan waktu yang ditentukan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 hari" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Kustom" }, - "maximumAccessCount": { - "message": "Hitungan Akses Maksimum" - }, - "maximumAccessCountDesc": { - "message": "Jika disetel, pengguna tidak dapat lagi mengakses Send ini setelah jumlah akses maksimum tercapai.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Secara opsional, minta kata sandi bagi pengguna untuk mengakses Send ini.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Tambahkan kata sandi tidak wajib untuk penerima untuk mengakses Send ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Catatan pribadi tentang Send ini.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Nonaktifkan Send ini sehingga tidak ada yang dapat mengaksesnya.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Salin tautan Send ini ke papan klip setelah disimpan.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Teks yang ingin Anda kirim." - }, - "sendHideText": { - "message": "Sembunyikan teks Send ini secara default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Hitungan Akses Saat Ini" - }, "createSend": { "message": "Buat Send Baru", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Sebelum kamu memulai" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Untuk menggunakan pemilih tanggal gaya kalender", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klik disini", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "untuk memunculkan jendela Anda.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Tanggal kedaluwarsa yang diberikan tidak valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Terjadi kesalahan saat menyimpan tanggal penghapusan dan kedaluwarsa Anda." }, - "hideEmail": { - "message": "Sembunyikan alamat surel dari penerima." - }, "hideYourEmail": { "message": "Sembunyikan alamat surel Anda dari penonton." }, - "sendOptionsPolicyInEffect": { - "message": "Satu atau lebih kebijakan organisasi mempengaruhi pengaturan feature Send anda." - }, "passwordPrompt": { "message": "Master password ditanyakan kembali" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Galat" }, - "regenerateUsername": { - "message": "Buat nama pengguna baru" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Buat nama pengguna baru" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Jenis nama pengguna" - }, "plusAddressedEmail": { "message": "Surel dengan alamat plus", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nama situs web" }, - "whatWouldYouLikeToGenerate": { - "message": "Apa yang ingin Anda buat?" - }, - "passwordType": { - "message": "Jenis kata sandi" - }, "service": { "message": "Layanan" }, @@ -3503,38 +3425,6 @@ "message": "Saklar lipat", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Impor data Anda ke Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Lindungi data LastPass Anda dan impor ke Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Simpan sebagai berkas yang tidak dienkripsi", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Impor ke Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Mengimpor...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data berhasil diimpor!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Gagal mengimpor. Periksa konsol untuk rinciannya.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Kesalahan jaringan ditemui ketika mengimpor.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domain alias" }, @@ -4086,7 +3976,10 @@ "message": "Kunci sandi dihapus" }, "autofillSuggestions": { - "message": "Saran isi otomatis" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Simpan benda login untuk situs ini ke isi otomatis" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 5b563d69f75..53b94a951c1 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -20,16 +20,16 @@ "message": "Crea account" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Nuovo in Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Accedi con passkey" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "Usa il Single Sign-On" }, "welcomeBack": { - "message": "Welcome back" + "message": "Bentornato" }, "setAStrongPassword": { "message": "Imposta una password robusta" @@ -120,7 +120,7 @@ "message": "Copia password" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "Copia passphrase" }, "copyNote": { "message": "Copia nota" @@ -153,13 +153,13 @@ "message": "Copia numero licenza" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Copia chiave privata" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Copia chiave pubblica" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Copia impronta" }, "copyCustomField": { "message": "Copia $FIELD$", @@ -177,7 +177,7 @@ "message": "Copia note" }, "fill": { - "message": "Fill", + "message": "Riempi", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "Riempi automaticamente identità" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Riempi codice di verifica" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Riempi Codice di Verifica", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -239,7 +239,7 @@ "message": "Aggiungi elemento" }, "accountEmail": { - "message": "Account email" + "message": "Email dell'account" }, "requestHint": { "message": "Richiedi suggerimento" @@ -443,7 +443,7 @@ "message": "Genera password" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "Genera passphrase" }, "regeneratePassword": { "message": "Rigenera password" @@ -454,9 +454,6 @@ "length": { "message": "Lunghezza" }, - "passwordMinLength": { - "message": "Lunghezza minima della password" - }, "uppercase": { "message": "Maiuscole (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,16 +525,12 @@ "minSpecial": { "message": "Minimo caratteri speciali" }, - "avoidAmbChar": { - "message": "Evita caratteri ambigui", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Evita caratteri ambigui", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "I requisiti di politica aziendale sono stati applicati alle opzioni del generatore.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -583,7 +576,7 @@ "message": "Note" }, "privateNote": { - "message": "Private note" + "message": "Nota privata" }, "note": { "message": "Nota" @@ -607,7 +600,7 @@ "message": "Avvia il sito web" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Apri sito web $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -640,7 +633,7 @@ "message": "Timeout della sessione" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Timeout cassaforte" }, "otherOptions": { "message": "Altre opzioni" @@ -658,13 +651,13 @@ "message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Cassaforte bloccata" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Il tuo account è bloccato" }, "or": { - "message": "or" + "message": "o" }, "unlock": { "message": "Sblocca" @@ -859,7 +852,7 @@ "message": "Accedi" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Accedi a Bitwarden" }, "restartRegistration": { "message": "Riprova la registrazione" @@ -895,10 +888,10 @@ "message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Rendi il tuo account più sicuro impostando l'autenticazione a due fattori nell'app web di Bitwarden." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Aprire web app?" }, "editedFolder": { "message": "Cartella salvata" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Mostra le identità nella sezione Scheda per riempirle automaticamente." }, + "clickToAutofillOnVault": { + "message": "Clicca gli oggetti da riempire dalla sezione Cassaforte" + }, "clearClipboard": { "message": "Cancella appunti", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1130,7 +1126,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Attenzione", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Sposta in organizzazione" }, - "share": { - "message": "Condividi" - }, "movedItemToOrg": { "message": "$ITEMNAME$ spostato in $ORGNAME$", "placeholders": { @@ -1213,7 +1206,7 @@ "message": "File" }, "fileToShare": { - "message": "File to share" + "message": "File da condividere" }, "selectFile": { "message": "Seleziona un file" @@ -1324,10 +1317,10 @@ "message": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Timeout autenticazione" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "La sessione di autenticazione è scaduta. Accedi di nuovo." }, "enterVerificationCodeEmail": { "message": "Inserisci il codice di verifica a 6 cifre inviato a $EMAIL$.", @@ -1447,7 +1440,7 @@ "message": "URL del server" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "URL server autogestito", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1479,10 +1472,10 @@ "message": "Mostra suggerimenti di riempimento automatico nei campi del modulo" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "Mostra identità come consigli" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "Mostra carte come consigli" }, "showInlineMenuOnIconSelectionLabel": { "message": "Mostra suggerimenti quando l'icona è selezionata" @@ -1775,7 +1768,7 @@ "message": "Identità" }, "typeSshKey": { - "message": "SSH key" + "message": "Chiave SSH" }, "newItemHeader": { "message": "Nuovo $TYPE$", @@ -1808,13 +1801,13 @@ "message": "Cronologia delle password" }, "generatorHistory": { - "message": "Generator history" + "message": "Cronologia generatore" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "Cancella cronologia generatore" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?" }, "back": { "message": "Indietro" @@ -1853,7 +1846,7 @@ "message": "Note sicure" }, "sshKeys": { - "message": "SSH Keys" + "message": "Chiavi SSH" }, "clear": { "message": "Cancella", @@ -1936,10 +1929,10 @@ "message": "Cancella cronologia" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Niente da mostrare" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "Non hai generato niente di recente" }, "remove": { "message": "Rimuovi" @@ -2000,16 +1993,16 @@ "message": "Sblocca con PIN" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "Imposta PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "Imposta PIN" }, "setYourPinCode": { "message": "Imposta il tuo codice PIN per sbloccare Bitwarden. Le tue impostazioni PIN saranno resettate se esci completamente dall'app." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "Il tuo PIN sarà usato per sbloccare Bitwarden invece della password principale. Il PIN sarà ripristinato se ti disconnetterai completamente da Bitwarden." }, "pinRequired": { "message": "Codice PIN obbligatorio." @@ -2024,7 +2017,7 @@ "message": "Sblocca con i dati biometrici" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Sblocca con password principale" }, "awaitDesktop": { "message": "In attesa di conferma dal desktop" @@ -2036,7 +2029,7 @@ "message": "Blocca con la password principale al riavvio del browser" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Richiedi password principale al riavvio del browser" }, "selectOneCollection": { "message": "Devi selezionare almeno una raccolta." @@ -2047,9 +2040,6 @@ "clone": { "message": "Clona" }, - "passwordGeneratorPolicyInEffect": { - "message": "Una o più politiche dell'organizzazione stanno influenzando le impostazioni del tuo generatore." - }, "passwordGenerator": { "message": "Generatore di password" }, @@ -2077,7 +2067,7 @@ "message": "Azione timeout cassaforte" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "Azione al timeout" }, "lock": { "message": "Blocca", @@ -2334,6 +2324,9 @@ "message": "Domini", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domini bloccati" + }, "excludedDomains": { "message": "Domini esclusi" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden non chiederà di salvare le credenziali di accesso per questi domini per tutti gli account sul dispositivo. Ricarica la pagina affinché le modifiche abbiano effetto." }, + "blockedDomainsDesc": { + "message": "Per questi siti, l'auto-completamento e funzionalità simili non saranno disponibili. Ricarica la pagina per applicare le modifiche." + }, + "autofillBlockedNoticeV2": { + "message": "La compilazione automatica è bloccata per questo sito." + }, + "autofillBlockedNoticeGuidance": { + "message": "Modifica questo nelle impostazioni" + }, "websiteItemLabel": { "message": "Sito $number$ (URI)", "placeholders": { @@ -2361,18 +2363,21 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Modifiche ai domini bloccati salvate" + }, "excludedDomainsSavedSuccess": { "message": "Modifiche del dominio escluso salvate" }, "limitSendViews": { - "message": "Limit views" + "message": "Limita visualizzazioni" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Nessuno potrà vedere questo Send al raggiungimento del limite.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ visualizzazioni rimaste", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2386,22 +2391,14 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Send details", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "searchSends": { - "message": "Cerca Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Aggiungi Send", + "message": "Dettagli Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { "message": "Testo" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Testo da condividere" }, "sendTypeFile": { "message": "File" @@ -2411,18 +2408,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Hide text by default" - }, - "maxAccessCountReached": { - "message": "Numero massimo di accessi raggiunto", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." + "message": "Nascondi testo come default" }, "expired": { "message": "Scaduto" }, - "pendingDeletion": { - "message": "In attesa di eliminazione" - }, "passwordProtected": { "message": "Protetto da password" }, @@ -2465,42 +2455,23 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "Sicuro di voler eliminare definitivamente questo Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { "message": "Modifica Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Che tipo di Send è questo?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Un nome intuitivo per descrivere questo Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Il file da inviare." - }, "deletionDate": { "message": "Data di eliminazione" }, - "deletionDateDesc": { - "message": "Il Send sarà eliminato definitivamente alla data e ora specificate.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "Il Send sarà cancellato definitivamente in questa data.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { "message": "Data di scadenza" }, - "expirationDateDesc": { - "message": "Se impostato, l'accesso a questo Send scadrà alla data e ora specificate.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 giorno" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Personalizzato" }, - "maximumAccessCount": { - "message": "Numero massimo di accessi" - }, - "maximumAccessCountDesc": { - "message": "Se impostata, gli utenti non potranno più accedere a questo Send una volta raggiunto il numero massimo di accessi.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Richiedi una password agli utenti per accedere a questo Send (facoltativo).", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Richiedi ai destinatari una password opzionale per aprire questo Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Note private sul Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Disattiva il Send per renderlo inaccessibile.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copia il link al Send negli appunti dopo averlo salvato.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Il testo che vuoi inviare." - }, - "sendHideText": { - "message": "Nascondi il testo di questo Send in modo predefinito.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Numero di accessi correnti" - }, "createSend": { "message": "Nuovo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2577,11 +2515,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "Il Send sarà disponibile a chiunque con il link per la prossima ora.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "Il Send sarà disponibile a chiunque con il link per le prossime $HOURS$ ore.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2591,11 +2529,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "Il Send sarà disponibile a chiunque con il link per il prossimo giorno.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "Il Send sarà disponibile a chiunque con il link per i prossimi $DAYS$ giorni.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2613,11 +2551,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogText": { - "message": "Pop out extension?", + "message": "Scollegare estensione?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out the extension to a new window.", + "message": "Per creare un file Send, devi scollegare l'estensione in una nuova finestra.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -2630,23 +2568,11 @@ "message": "Per scegliere un file usando Safari, apri una nuova finestra cliccando questo banner." }, "popOut": { - "message": "Pop out" + "message": "Scollega" }, "sendFileCalloutHeader": { "message": "Prima di iniziare" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Per usare un selettore di date stile calendario", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "clicca qui", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "per aprire la finestra.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "La data di scadenza fornita non è valida." }, @@ -2662,14 +2588,8 @@ "dateParsingError": { "message": "Si è verificato un errore durante il salvataggio delle date di eliminazione e scadenza." }, - "hideEmail": { - "message": "Nascondi il mio indirizzo email dai destinatari." - }, "hideYourEmail": { - "message": "Hide your email address from viewers." - }, - "sendOptionsPolicyInEffect": { - "message": "Una o più politiche dell'organizzazione stanno influenzando le tue opzioni di Send." + "message": "Nascondi il tuo indirizzo email ai visualizzatori." }, "passwordPrompt": { "message": "Richiedi di inserire la password principale di nuovo per visualizzare questo elemento" @@ -2726,7 +2646,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "di $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2745,7 +2665,7 @@ "message": "Minuti" }, "vaultTimeoutPolicyAffectingOptions": { - "message": "Enterprise policy requirements have been applied to your timeout options" + "message": "I requisiti di politica aziendale sono stati applicati alle opzioni di timeout" }, "vaultTimeoutPolicyInEffect": { "message": "Le politiche della tua organizzazione hanno impostato il timeout massimo consentito della tua cassaforte su $HOURS$ ore e $MINUTES$ minuti.", @@ -2761,7 +2681,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "Al massimo $HOURS$ ora/e e $MINUTES$ minuto/i.", "placeholders": { "hours": { "content": "$1", @@ -2774,7 +2694,7 @@ } }, "vaultTimeoutPolicyMaximumError": { - "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "message": "Il timeout supera la restrizione impostata dalla tua organizzazione: massimo $HOURS$ ora/e e $MINUTES$ minuto/i", "placeholders": { "hours": { "content": "$1", @@ -2884,17 +2804,28 @@ "error": { "message": "Errore" }, - "regenerateUsername": { - "message": "Rigenera nome utente" + "decryptionError": { + "message": "Errore di decifrazione" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden non può decifrare gli elementi elencati di seguito." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contatta il cliente correttamente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "per evitare ulteriori perdite di dati.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Genera nome utente" }, "generateEmail": { - "message": "Generate email" + "message": "Genera email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Il valore deve essere compreso tra $MIN$ e $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2908,7 +2839,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2918,7 +2849,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Tipo di nome utente" - }, "plusAddressedEmail": { "message": "Indirizzo email alternativo", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nome sito web" }, - "whatWouldYouLikeToGenerate": { - "message": "Cosa vuoi generare?" - }, - "passwordType": { - "message": "Tipo di password" - }, "service": { "message": "Servizio" }, @@ -2968,11 +2890,11 @@ "message": "Genera un alias email con un servizio di inoltro esterno." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Dominio email", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Scegli un dominio supportato dal servizio selezionato", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { @@ -3175,25 +3097,25 @@ "message": "Invia notifica di nuovo" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Visualizza tutte le opzioni di accesso" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Visualizza tutte le opzioni di accesso" }, "notificationSentDevice": { "message": "Una notifica è stata inviata al tuo dispositivo." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Una notifica è stata inviata al tuo dispositivo" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Sarai notificato una volta che la richiesta sarà approvata" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Bisogno di un'altra opzione?" }, "loginInitiated": { "message": "Accesso avviato" @@ -3289,16 +3211,16 @@ "message": "Si apre in una nuova finestra" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Ricorda questo dispositivo per rendere immediati i futuri accessi" }, "deviceApprovalRequired": { "message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Approvazione dispositivo richiesta" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Seleziona un'opzione di approvazione sotto" }, "rememberThisDevice": { "message": "Ricorda questo dispositivo" @@ -3374,7 +3296,7 @@ "message": "Email utente mancante" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Email utente attiva non trovata. Logout in corso." }, "deviceTrusted": { "message": "Dispositivo fidato" @@ -3503,38 +3425,6 @@ "message": "Comprimi/espandi", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importare i tuoi dati su Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteggere i tuoi dati LastPass e importarli su Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salva come file non crittografato", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importa su Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importazione in corso...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dati importati!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Errore durante l'importazione. Controlla la console per i dettagli.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Errore di connessione durante l'importazione.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Dominio alias" }, @@ -3585,11 +3475,11 @@ "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "Codice di Verifica One-Time a tempo", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "Tempo rimasto prima che l'attuale TOTP scada", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -3818,10 +3708,10 @@ "message": "Passkey" }, "accessing": { - "message": "Accessing" + "message": "Accedendo a" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Accesso effettuato!" }, "passkeyNotCopied": { "message": "La passkey non sarà copiata" @@ -3848,7 +3738,7 @@ "message": "Nessun login corrispondente per questo sito" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Cerca o salva la passkey come nuovo login" }, "confirm": { "message": "Conferma" @@ -4086,7 +3976,10 @@ "message": "Passkey rimossa" }, "autofillSuggestions": { - "message": "Suggerimenti per il riempimento automatico" + "message": "Suggerimenti riempimento automatico" + }, + "itemSuggestions": { + "message": "Elementi suggeriti" }, "autofillSuggestionsTip": { "message": "Salva un elemento di accesso per questo sito da riempire automaticamente" @@ -4315,13 +4208,13 @@ "message": "Filtri" }, "filterVault": { - "message": "Filter vault" + "message": "Filtra cassaforte" }, "filterApplied": { - "message": "One filter applied" + "message": "Un filtro applicato" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ filtri applicati", "placeholders": { "count": { "content": "$1", @@ -4435,7 +4328,7 @@ "message": "Abilita animazioni" }, "showAnimations": { - "message": "Show animations" + "message": "Mostra animazioni" }, "addAccount": { "message": "Aggiungi account" @@ -4653,23 +4546,17 @@ "message": "Posizione elemento" }, "fileSend": { - "message": "File Send" + "message": "Send di File" }, "fileSends": { "message": "Send File" }, "textSend": { - "message": "Text Send" + "message": "Send di Testo" }, "textSends": { "message": "Send Testo" }, - "bitwardenNewLook": { - "message": "Bitwarden ha un nuovo look!" - }, - "bitwardenNewLookDesc": { - "message": "È più facile e intuitivo che mai utilizzare il riempimento automatico e cercare dalla scheda Cassaforte. Dai un'occhiata!" - }, "accountActions": { "message": "Azioni dell'account" }, @@ -4677,7 +4564,7 @@ "message": "Mostra il numero di suggerimenti di riempimento automatico sull'icona dell'estensione" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Mostra azioni di copia rapida nella Cassaforte" }, "systemDefault": { "message": "Predefinito del sistema" @@ -4686,37 +4573,37 @@ "message": "I requisiti della policy aziendale sono stati applicati a questa impostazione" }, "sshPrivateKey": { - "message": "Private key" + "message": "Chiave privata" }, "sshPublicKey": { - "message": "Public key" + "message": "Chiave pubblica" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Impronta digitale" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Tipo di chiave" }, "sshKeyAlgorithmED25519": { "message": "ED25519" }, "sshKeyAlgorithmRSA2048": { - "message": "RSA 2048-Bit" + "message": "RSA a 2048 bit" }, "sshKeyAlgorithmRSA3072": { - "message": "RSA 3072-Bit" + "message": "RSA a 3072 bit" }, "sshKeyAlgorithmRSA4096": { - "message": "RSA 4096-Bit" + "message": "RSA a 4096 bit" }, "retry": { - "message": "Retry" + "message": "Riprova" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "Il timeout personalizzato minimo è 1 minuto." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "Sono disponibili ulteriori contenuti" }, "fileSavedToDevice": { "message": "File salvato sul dispositivo. Gestisci dai download del dispositivo." @@ -4748,23 +4635,50 @@ "noEditPermissions": { "message": "Non hai i permessi per modificare questo elemento" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Lo sblocco biometrico non è disponibile perché è necessario prima sbloccare con PIN o parola d'accesso." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Lo sblocco biometrico non è attualmente disponibile." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Lo sblocco biometrico non è disponibile perché l'app desktop Bitwarden è chiusa." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Lo sblocco biometrico non è disponibile perché non è abilitato per $EMAIL$ nell'app desktop Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Lo sblocco biometrico non è attualmente disponibile per un motivo sconosciuto." + }, "authenticating": { - "message": "Authenticating" + "message": "Autenticazione" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "Riempi password generata", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "Password rigenerata", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Salvare il login su Bitwarden?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Spazio", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { @@ -4776,157 +4690,157 @@ "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Punto esclamativo", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Chiocciola", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "Cancelletto", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Simbolo del dollaro", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Segno di percentuale", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "Accento circonflesso", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "E commerciale", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Asterisco", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Parentesi sinistra", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Parentesi destra", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Trattino basso", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Trattino", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Più", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "Uguale", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Parentesi graffa aperta", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Parentesi graffa chiusa", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Parentesi quadra aperta", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Parentesi quadra chiusa", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "Barra verticale", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Barra rovesciata", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Due punti", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Punto e virgola", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Doppi apici", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Apostrofo", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Minore", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Maggiore", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Virgola", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Punto", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Punto interrogativo", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Slash", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Minuscolo" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Maiuscolo" }, "generatedPassword": { - "message": "Generated password" + "message": "Password generata" }, "compactMode": { - "message": "Compact mode" + "message": "Modalità compatta" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Avviso importante" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Imposta accesso in due passaggi" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden invierà un codice all'email del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere." }, "remindMeLater": { - "message": "Remind me later" + "message": "Ricordamelo più tardi" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Riesci ancora ad accedere a questa email, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4935,24 +4849,24 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "No, non riesco" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Sì, riesco ad accedere a questa email" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Attiva accesso in due passaggi" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Cambia l'email dell'account" }, "extensionWidth": { - "message": "Extension width" + "message": "Larghezza estensione" }, "wide": { - "message": "Wide" + "message": "Larga" }, "extraWide": { - "message": "Extra wide" + "message": "Molto larga" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index f5998a02f87..1824c9314ee 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -454,9 +454,6 @@ "length": { "message": "長さ" }, - "passwordMinLength": { - "message": "パスワードの最低文字数" - }, "uppercase": { "message": "大文字(A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "記号の最小数" }, - "avoidAmbChar": { - "message": "あいまいな文字を省く", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "あいまいな文字を避ける", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "自動入力を簡単にするために、タブページに ID アイテムを表示します" }, + "clickToAutofillOnVault": { + "message": "保管庫で、自動入力するアイテムをクリックしてください" + }, "clearClipboard": { "message": "クリップボードの消去", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "組織に移動" }, - "share": { - "message": "共有" - }, "movedItemToOrg": { "message": "$ITEMNAME$ を $ORGNAME$ に移動しました", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "複製" }, - "passwordGeneratorPolicyInEffect": { - "message": "一つ以上の組織のポリシーがパスワード生成の設定に影響しています。" - }, "passwordGenerator": { "message": "パスワード生成ツール" }, @@ -2334,6 +2324,9 @@ "message": "ドメイン", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "除外するドメイン" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden はログインしているすべてのアカウントで、これらのドメインのログイン情報を保存するよう要求しません。 変更を有効にするにはページを更新する必要があります。" }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "ウェブサイト $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "除外ドメインの変更を保存しました" }, @@ -2389,14 +2394,6 @@ "message": "Send の詳細", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Send を検索", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Send を追加", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "テキスト" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "デフォルトでテキストを隠す" }, - "maxAccessCountReached": { - "message": "最大アクセス数に達しました", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "有効期限切れ" }, - "pendingDeletion": { - "message": "削除の保留中" - }, "passwordProtected": { "message": "パスワード保護あり" }, @@ -2472,24 +2462,9 @@ "message": "Send を編集", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "この Send の種類は何ですか?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "この Send を説明するわかりやすい名前", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "送信するファイル" - }, "deletionDate": { "message": "削除日時" }, - "deletionDateDesc": { - "message": "Send は指定された日時に完全に削除されます。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send はこの日付で完全に削除されます。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "有効期限" }, - "expirationDateDesc": { - "message": "設定されている場合、この Send へのアクセスは指定された日時に失効します。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1日" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "カスタム" }, - "maximumAccessCount": { - "message": "最大アクセス数" - }, - "maximumAccessCountDesc": { - "message": "設定されている場合、最大アクセス数に達するとユーザーはこの Send にアクセスできなくなります。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "必要に応じて、ユーザーがこの Send にアクセスするためのパスワードを要求します。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "受信者がこの Send にアクセスするための任意のパスワードを追加します。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "この Send に関するプライベートメモ", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "誰もアクセスできないように、この Send を無効にする", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "保存時にこの Send のリンクをクリップボードにコピーする", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "送信したいテキスト" - }, - "sendHideText": { - "message": "この Send のテキストをデフォルトで非表示にする", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "現在のアクセス数" - }, "createSend": { "message": "新しい Send を作成", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "はじめる前に" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "カレンダースタイルの日付ピッカーを使用するには", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "こちらをクリック", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "してください。", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "入力された有効期限は正しくありません。" }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "削除と有効期限の保存中にエラーが発生しました。" }, - "hideEmail": { - "message": "メールアドレスを受信者に表示しない" - }, "hideYourEmail": { "message": "閲覧者にメールアドレスを見せないようにします。" }, - "sendOptionsPolicyInEffect": { - "message": "一つ以上の組織ポリシーが Send の設定に影響しています。" - }, "passwordPrompt": { "message": "マスターパスワードの再要求" }, @@ -2884,8 +2804,19 @@ "error": { "message": "エラー" }, - "regenerateUsername": { - "message": "ユーザー名を再生成" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "ユーザー名を生成" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "ユーザー名の種類" - }, "plusAddressedEmail": { "message": "プラス付きのメールアドレス", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "ウェブサイト名" }, - "whatWouldYouLikeToGenerate": { - "message": "何を生成しますか?" - }, - "passwordType": { - "message": "パスワードの種類" - }, "service": { "message": "サービス" }, @@ -3503,38 +3425,6 @@ "message": "開く/閉じる", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Bitwarden にデータをインポートしますか?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass データを Bitwarden にインポートしますか?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "暗号化されていないファイルとして保存", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden にインポート", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "インポート中...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "データをインポートしました!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "インポート中にエラーが発生しました。詳細はコンソールを確認してください。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "インポート中にネットワークエラーが発生しました。", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "エイリアスドメイン" }, @@ -4086,7 +3976,10 @@ "message": "パスキーを削除しました" }, "autofillSuggestions": { - "message": "候補を自動入力する" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "自動入力するためにこのサイトのログインアイテムを保存します" @@ -4664,12 +4557,6 @@ "textSends": { "message": "テキスト Send" }, - "bitwardenNewLook": { - "message": "Bitwarden が新しい外観になりました。" - }, - "bitwardenNewLookDesc": { - "message": "保管庫タブからの自動入力と検索がこれまで以上に簡単で直感的になりました。" - }, "accountActions": { "message": "アカウントの操作" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "このアイテムを編集する権限がありません" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "認証中" }, @@ -4911,22 +4825,22 @@ "message": "ベータ" }, "importantNotice": { - "message": "Important notice" + "message": "重要なお知らせ" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "2段階認証を設定する" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。" }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。" }, "remindMeLater": { - "message": "Remind me later" + "message": "後で再通知" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "新しいメールアドレス $EMAIL$ はあなたが管理しているものですか?", "placeholders": { "email": { "content": "$1", @@ -4935,16 +4849,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "いいえ、違います。" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "はい、メールアドレスには私が確実にアクセスできます" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "2段階認証によるログインを有効にする" }, "changeAcctEmail": { - "message": "Change account email" + "message": "アカウントのメールアドレスを変更する" }, "extensionWidth": { "message": "拡張機能の幅" diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 8cef6ece782..1f8f2766e7b 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -454,9 +454,6 @@ "length": { "message": "სიგრძე" }, - "passwordMinLength": { - "message": "პაროლის მინიმალური სიგრძე" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "ბუფერის გასუფთავება", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "გაზიარება" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "კლონი" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "დომენები", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "ტექსტი" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "ვადაგასულია" }, - "pendingDeletion": { - "message": "ელოდება წაშლას" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "ვადა" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 დღე" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "განსხვავებული" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "აქ დააწკაპუნეთ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "შეცდომა" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "სერვისი" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "შემოტანა...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "ავთენტიკაცია" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 8f7673803cb..a7913bbfc9b 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 7507e254447..ce85c7ea820 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -454,9 +454,6 @@ "length": { "message": "ಉದ್ದ" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "ಕನಿಷ್ಠ ವಿಶೇಷ" }, - "avoidAmbChar": { - "message": "ಅಸ್ಪಷ್ಟ ಅಕ್ಷರಗಳನ್ನು ತಪ್ಪಿಸಿ", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "ಕ್ಲಿಪ್‌ಬೋರ್ಡ್ ತೆರವುಗೊಳಿಸಿ", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "ಸಂಸ್ಥೆಗೆ ಸರಿಸಿ" }, - "share": { - "message": "ಹಂಚಿಕೊಳ್ಳಿ" - }, "movedItemToOrg": { "message": "$ITEMNAME$ ಅನ್ನು $ORGNAME$ ಗೆ ಸರಿಸಲಾಗಿದೆ", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "ಕ್ಲೋನ್" }, - "passwordGeneratorPolicyInEffect": { - "message": "ಒಂದು ಅಥವಾ ಹೆಚ್ಚಿನ ಸಂಸ್ಥೆ ನೀತಿಗಳು ನಿಮ್ಮ ಜನರೇಟರ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರುತ್ತವೆ" - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "ಹೊರತುಪಡಿಸಿದ ಡೊಮೇನ್ಗಳು" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "ಹುಡುಕಾಟ ಕಳುಹಿಸುತ್ತದೆ", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "ಕಳುಹಿಸು ಸೇರಿಸಿ", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "ಪಠ್ಯ" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "ಗರಿಷ್ಠ ಪ್ರವೇಶ ಎಣಿಕೆ ತಲುಪಿದೆ", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "ಅವಧಿ ಮೀರಿದೆ" }, - "pendingDeletion": { - "message": "ಅಳಿಸುವಿಕೆ ಬಾಕಿ ಉಳಿದಿದೆ" - }, "passwordProtected": { "message": "ಪಾಸ್ವರ್ಡ್ ರಕ್ಷಿತ" }, @@ -2472,24 +2462,9 @@ "message": "ಕಳುಹಿಸು ಸಂಪಾದಿಸಿ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "ಇದು ಯಾವ ರೀತಿಯ ಕಳುಹಿಸುತ್ತದೆ?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "ಇದನ್ನು ಕಳುಹಿಸಲು ವಿವರಿಸಲು ಸ್ನೇಹಪರ ಹೆಸರು.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "ನೀವು ಕಳುಹಿಸಲು ಬಯಸುವ ಫೈಲ್." - }, "deletionDate": { "message": "ಅಳಿಸುವ ದಿನಾಂಕ" }, - "deletionDateDesc": { - "message": "ಕಳುಹಿಸಿದ ದಿನಾಂಕ ಮತ್ತು ಸಮಯದ ಮೇಲೆ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಶಾಶ್ವತವಾಗಿ ಅಳಿಸಲಾಗುತ್ತದೆ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "ಮುಕ್ತಾಯ ದಿನಾಂಕ" }, - "expirationDateDesc": { - "message": "ಹೊಂದಿಸಿದ್ದರೆ, ಈ ಕಳುಹಿಸುವಿಕೆಯ ಪ್ರವೇಶವು ನಿಗದಿತ ದಿನಾಂಕ ಮತ್ತು ಸಮಯದ ಮೇಲೆ ಮುಕ್ತಾಯಗೊಳ್ಳುತ್ತದೆ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 ದಿನ" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "ಕಸ್ಟಮ್" }, - "maximumAccessCount": { - "message": "ಗರಿಷ್ಠ ಪ್ರವೇಶ ಎಣಿಕೆ" - }, - "maximumAccessCountDesc": { - "message": "ಹೊಂದಿಸಿದ್ದರೆ, ಗರಿಷ್ಠ ಪ್ರವೇಶ ಎಣಿಕೆ ತಲುಪಿದ ನಂತರ ಬಳಕೆದಾರರಿಗೆ ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಬಳಕೆದಾರರಿಗೆ ಪಾಸ್‌ವರ್ಡ್ ಐಚ್ ಗತ್ಯವಿದೆ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "ಈ ಕಳುಹಿಸುವ ಬಗ್ಗೆ ಖಾಸಗಿ ಟಿಪ್ಪಣಿಗಳು.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "ಇದನ್ನು ಕಳುಹಿಸುವುದನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ ಇದರಿಂದ ಯಾರೂ ಅದನ್ನು ಪ್ರವೇಶಿಸಲಾಗುವುದಿಲ್ಲ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "ಉಳಿಸಿದ ನಂತರ ಈ ಕಳುಹಿಸುವ ಲಿಂಕ್ ಅನ್ನು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ಗೆ ನಕಲಿಸಿ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "ನೀವು ಕಳುಹಿಸಲು ಬಯಸುವ ಪಠ್ಯ." - }, - "sendHideText": { - "message": "ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಈ ಕಳುಹಿಸುವ ಪಠ್ಯವನ್ನು ಮರೆಮಾಡಿ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "ಪ್ರಸ್ತುತ ಪ್ರವೇಶ ಎಣಿಕೆ" - }, "createSend": { "message": "ಹೊಸ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ರಚಿಸಿ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "ನೀವು ಪ್ರಾರಂಭಿಸುವ ಮೊದಲು" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "ಕ್ಯಾಲೆಂಡರ್ ಶೈಲಿಯ ದಿನಾಂಕ ಆಯ್ದುಕೊಳ್ಳುವಿಕೆಯನ್ನು ಬಳಸಲು", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "ಕ್ಲಿಕ್ ಮಾಡಿ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "ನಿಮ್ಮ ವಿಂಡೋವನ್ನು ಪಾಪ್ಔಟ್ ಮಾಡಲು.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "ಒದಗಿಸಿದ ಮುಕ್ತಾಯ ದಿನಾಂಕವು ಮಾನ್ಯವಾಗಿಲ್ಲ." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "ನಿಮ್ಮ ಅಳಿಸುವಿಕೆ ಮತ್ತು ಮುಕ್ತಾಯ ದಿನಾಂಕಗಳನ್ನು ಉಳಿಸುವಲ್ಲಿ ದೋಷ ಕಂಡುಬಂದಿದೆ." }, - "hideEmail": { - "message": "ಸ್ವೀಕರಿಸುವವರಿಂದ ನನ್ನ ಇಮೇಲ್ ವಿಳಾಸವನ್ನು ಮರೆಮಾಡಿ." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "ಒಂದು ಅಥವಾ ಹೆಚ್ಚಿನ ಸಂಸ್ಥೆಯ ನೀತಿಗಳು ನಿಮ್ಮ ಕಳುಹಿಸುವ ಆಯ್ಕೆಗಳ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರುತ್ತವೆ." - }, "passwordPrompt": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಮರು-ಪ್ರಾಂಪ್ಟ್" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 6ab1a467475..c264492e0d2 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -454,9 +454,6 @@ "length": { "message": "길이" }, - "passwordMinLength": { - "message": "최소 비밀번호 길이" - }, "uppercase": { "message": "대문자 (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "특수 문자 최소 개수" }, - "avoidAmbChar": { - "message": "모호한 문자 사용 안 함", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "모호한 문자 사용 안 함", "description": "Label for the avoid ambiguous characters checkbox." @@ -994,7 +987,7 @@ "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, "showCardsInVaultView": { - "message": "보관함 보기에서 카드 자동완성 제안를 표시" + "message": "보관함 보기에서 카드 자동 완성 제안을 표시" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" @@ -1003,7 +996,7 @@ "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, "showIdentitiesInVaultView": { - "message": "보관함 보기에서 신원들의 자동완성 제안을 표시" + "message": "보관함 보기에서 신원들의 자동 완성 제안을 표시" }, "showIdentitiesCurrentTab": { "message": "탭 페이지에 신원들을 표시" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "간편한 자동완성을 위해 탭에 신원 항목들을 나열" }, + "clickToAutofillOnVault": { + "message": "보관함 보기에서 항목을 클릭하여 자동 완성" + }, "clearClipboard": { "message": "클립보드 비우기", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1111,10 +1107,10 @@ "message": "이 비밀번호는 이 파일을 파일 내보내거나, 가져오는데 사용됩니다." }, "accountRestrictedOptionDescription": { - "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으로 가져오기를 제한해보세요. " + "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으만 가져오기를 제한합니다." }, "passwordProtectedOptionDescription": { - "message": "파일 비밀번호를 설정하여, 내보내기를 암호화하고, 해독에 그 파일 비밀번호를 사용하는 Bitwarden계정에 가져오세요." + "message": "파일 비밀번호를 설정하여 내보내기를 암호화하고, 어느 Bitwarden 계정으로든 해독에 그 파일 비밀번호를 사용하여 가져오세요." }, "exportTypeHeading": { "message": "내보내기 유형" @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "조직으로 이동하기" }, - "share": { - "message": "공유" - }, "movedItemToOrg": { "message": "$ITEMNAME$이(가) $ORGNAME$(으)로 이동됨", "placeholders": { @@ -1485,7 +1478,7 @@ "message": "카드를 제안으로 표시" }, "showInlineMenuOnIconSelectionLabel": { - "message": "아이콘을 선택하면 제안이 표시됩니다." + "message": "아이콘을 선택할 때 제안을 표시" }, "showInlineMenuOnFormFieldsDescAlt": { "message": "로그인한 모든 계정에 적용" @@ -2047,9 +2040,6 @@ "clone": { "message": "복제" }, - "passwordGeneratorPolicyInEffect": { - "message": "하나 이상의 단체 정책이 생성기 규칙에 영항을 미치고 있습니다." - }, "passwordGenerator": { "message": "비밀번호 생성기" }, @@ -2334,6 +2324,9 @@ "message": "도메인", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "제외된 도메인" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "BItwarden은 로그인한 모든 계정에 대해 이러한 도메인에 대한 로그인 세부 정보를 저장하도록 요청하지 않습니다. 변경 사항을 적용하려면 페이지를 새로 고쳐야 합니다" }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "웹사이트 $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "제외된 도메인 변경 사항 저장됨" }, @@ -2389,14 +2394,6 @@ "message": "보내기 세부 정보", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": " Send 검색", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": " Send 추가", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "텍스트" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "기본적으로 텍스트 숨기기" }, - "maxAccessCountReached": { - "message": "최대 접근 횟수 도달", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "만료됨" }, - "pendingDeletion": { - "message": "삭제 대기 중" - }, "passwordProtected": { "message": "비밀번호로 보호됨" }, @@ -2472,24 +2462,9 @@ "message": " Send 편집", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "어떤 유형의 Send인가요?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "이 Send을 설명할 이름", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "전송하려는 파일" - }, "deletionDate": { "message": "삭제 날짜" }, - "deletionDateDesc": { - "message": "이 Send가 정해진 일시에 영구적으로 삭제됩니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "이 Send가 이 날짜에 영구적으로 삭제됩니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "만료 날짜" }, - "expirationDateDesc": { - "message": "설정할 경우, 이 Send에 대한 접근 권한이 정해진 일시에 만료됩니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1일" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "사용자 지정" }, - "maximumAccessCount": { - "message": "최대 접근 횟수" - }, - "maximumAccessCountDesc": { - "message": "설정할 경우, 최대 접근 횟수에 도달할 때 이 Send에 접근할 수 없게 됩니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "이 Send에 접근하기 위해 암호를 입력하도록 선택적으로 요구합니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "수신자가 이 Send에 액세스할 수 있도록 비밀번호 옵션를 추가합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "이 Send에 대한 비공개 메모", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "이 Send를 비활성화하여 아무도 접근할 수 없게 합니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "저장할 때 이 Send의 링크를 클립보드에 복사합니다.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "전송하려는 텍스트" - }, - "sendHideText": { - "message": "이 Send의 텍스트를 기본적으로 숨김", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "현재 접근 횟수" - }, "createSend": { "message": "새 Send 생성", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "시작하기 전에" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "달력을 보고 날짜를 선택하려면", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "여기를 클릭하여", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "확장 프로그램을 새 창에서 여세요.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "제공된 만료 날짜가 유효하지 않습니다." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "삭제 날짜와 만료 날짜를 저장하는 도중 오류가 발생했습니다." }, - "hideEmail": { - "message": "받는 사람으로부터 나의 이메일 주소 숨기기" - }, "hideYourEmail": { "message": "사람들로부터 이메일 주소를 숨기세요." }, - "sendOptionsPolicyInEffect": { - "message": "하나 이상의 단체 정책이 Send 설정에 영향을 미치고 있습니다." - }, "passwordPrompt": { "message": "마스터 비밀번호 재확인" }, @@ -2884,8 +2804,19 @@ "error": { "message": "오류" }, - "regenerateUsername": { - "message": "아이디 재생성" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "아이디 생성" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "아이디 유형" - }, "plusAddressedEmail": { "message": "추가 이메일", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "웹사이트 이름" }, - "whatWouldYouLikeToGenerate": { - "message": "무엇을 생성하실건가요?" - }, - "passwordType": { - "message": "비밀번호 유형" - }, "service": { "message": "서비스" }, @@ -3503,38 +3425,6 @@ "message": "토글이 붕괴됨", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "데이터를 Bitwarden으로 가져오시겠습니까?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass 데이터를 보호하고 Bitwarden으로 가져오시겠습니까?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "암호화되지 않은 파일로 저장", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden으로 가져오기", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "가져오는 중...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "데이터 가져오기 성공!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "가져오는 중 오류가 발생했습니다. 자세한 내용은 콘솔을 확인하세요.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "가져오기 중에 네트워크 오류가 발생했습니다.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "도메인 별칭" }, @@ -4086,7 +3976,10 @@ "message": "패스키 제거됨" }, "autofillSuggestions": { - "message": "자동 완성 제안" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "이 사이트에서 자동으로 작성할 로그인 항목 저장" @@ -4664,12 +4557,6 @@ "textSends": { "message": "텍스트 Send" }, - "bitwardenNewLook": { - "message": "Bitwarden이 새로운 모습으로 돌아왔습니다!" - }, - "bitwardenNewLookDesc": { - "message": "보관함 탭에서 자동 완성하고 검색하는 것이 그 어느 때보다 쉽고 직관적입니다. 둘러보세요!" - }, "accountActions": { "message": "계정 작업" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "아이템을 수정할 권한이 없습니다." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "인증 중" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index f55fb8b3e25..f76ae921425 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Ilgis" }, - "passwordMinLength": { - "message": "Minimalus slaptažodžio ilgis" - }, "uppercase": { "message": "Didžiosiomis (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Mažiausiai simbolių" }, - "avoidAmbChar": { - "message": "Vengti dviprasmiškų simbolių", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Pateikti tapatybės elementų skirtuko puslapyje, kad būtų lengva automatiškai užpildyti." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Išvalyti iškarpinę", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Perkelti į organizaciją" }, - "share": { - "message": "Bendrinti" - }, "movedItemToOrg": { "message": "$ITEMNAME$ perkelta(s) į $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Klonuoti" }, - "passwordGeneratorPolicyInEffect": { - "message": "Viena ar daugiau organizacijos politikų turi įtakos Jūsų generatoriaus nustatymams." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domenai", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Išskirti domenai" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "„Bitwarden“ neprašys išsaugoti prisijungimo detalių šiems domenams, visose prisijungusiose paskyrose. Turite atnaujinti puslapį, kad pokyčiai pradėtų galioti." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Ieškoti „Sends“", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Pridėti „Send“", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekstas" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Pasiektas maksimalus prisijungimų skaičius", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Nebegalioja" }, - "pendingDeletion": { - "message": "Laukiama ištrynimo" - }, "passwordProtected": { "message": "Apsaugota slaptažodžiu" }, @@ -2472,24 +2462,9 @@ "message": "Redaguoti „Send“", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Kokio tai tipo „Send“?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Draugiškas pavadinimas, apibūdinantis šį „Send“.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Failas, kurį norite siųsti." - }, "deletionDate": { "message": "Ištrynimo data" }, - "deletionDateDesc": { - "message": "Nurodytos datos ir laiko metu „Send“ bus bus ištrinta visam laikui.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Galiojimo data" }, - "expirationDateDesc": { - "message": "Jei nustatyta, prieiga prie šio „Send“ nustos galioti nurodyta data ir laikui.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 d" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Pasirinktinis" }, - "maximumAccessCount": { - "message": "Maksimalus prisijungimų skaičius" - }, - "maximumAccessCountDesc": { - "message": "Jei nustatyta, vartotojai nebegalės pasiekti šio „Send“, kai bus pasiektas maksimalus prisijungimų skaičius.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Pasirinktinai reikalauti slaptažodžio, kad vartotojai galėtų pasiekti šį „Send“.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Asmeninės pastabos apie šį „Send“.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Išjunkite šį „Send“, kad niekas negalėtų jo pasiekti.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Išsaugant šį „Send“ nukopijuokite nuorodą į mainų sritį.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Tekstas, kurį norite siųsti." - }, - "sendHideText": { - "message": "Pagal numatytuosius nustatymus slėpti šį „Send“ tekstą.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Dabartinis prisijungimų skaičius" - }, "createSend": { "message": "Naujas Siuntinys", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Prieš pradedant" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Norint pasinaudoti kalendoriaus stiliaus datos pasirinkikliu", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "spauskite čia", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "iškelti atskirame lange.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Nurodytas galiojimo laikas negalioja." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Išsaugant jūsų ištrynimo ir galiojimo datas įvyko klaida." }, - "hideEmail": { - "message": "Slėpti mano el. pašto adresą nuo gavėjų." - }, "hideYourEmail": { "message": "Slėpkite savo el. pašto adresą nuo žiūrėtojų." }, - "sendOptionsPolicyInEffect": { - "message": "Viena ar daugiau organizacijos politikų turi įtakos Jūsų „Send“ nustatymams." - }, "passwordPrompt": { "message": "Iš naujo prašoma pagrindinio slaptažodžio" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Klaida" }, - "regenerateUsername": { - "message": "Pergeneruoti vartotojo vardą iš naujo" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generuoti vartotojo vardą" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Vartotojo prisijungimo vardo tipas" - }, "plusAddressedEmail": { "message": "Plius adresuotas el. paštas", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Svetainės pavadinimas" }, - "whatWouldYouLikeToGenerate": { - "message": "Ką norėtumėte sugeneruoti?" - }, - "passwordType": { - "message": "Slaptažodžio tipas" - }, "service": { "message": "Paslauga" }, @@ -3503,38 +3425,6 @@ "message": "Perjungti sutrumpinimą", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importuoti duomenis į Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Apsaugoti LastPass duomenis ir importuoti į Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Išsaugoti kaip neužšifruotą failą", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importuoti į Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importuojama...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Duomenys sėkmingai importuoti.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Klaida importuojant. Išsamesnės informacijos patikrink konsolėje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Importuojant įvyko tinklo klaida.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domeno slapyvardis" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 31bdcd59e45..d18fbb92039 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Garums" }, - "passwordMinLength": { - "message": "Mazākais pieļaujamais paroles garums" - }, "uppercase": { "message": "Lielie burti (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Mazākais pieļaujamais īpašo rakstzīmju skaits" }, - "avoidAmbChar": { - "message": "Izvairīties no viegli sajaucamām rakstzīmēm", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Izvairīties no viegli sajaucamām rakstzīmēm", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Attēlot identitātes ciļņu lapā vieglākai aizpildei." }, + "clickToAutofillOnVault": { + "message": "Glabātavas skatā jāklikšķina uz vienumiem, lai automātiski aizpildītu" + }, "clearClipboard": { "message": "Notīrīt starpliktuvi", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Pārvietot uz apvienību" }, - "share": { - "message": "Kopīgot" - }, "movedItemToOrg": { "message": "$ITEMNAME$ pārvietots uz $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Pavairot" }, - "passwordGeneratorPolicyInEffect": { - "message": "Viens vai vairāki apvienības nosacījumi ietekmē veidotāja iestatījumus." - }, "passwordGenerator": { "message": "Paroļu veidotājs" }, @@ -2334,6 +2324,9 @@ "message": "Domēna vārdi", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Liegtie domēna vārdi" + }, "excludedDomains": { "message": "Izņēmuma domēni" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nevaicās saglabāt pieteikšanās datus visiem šī domēna kontiem, kuri ir pieteikušies. Ir jāpārlādē lapa, lai iedarbotos izmaiņas." }, + "blockedDomainsDesc": { + "message": "Automātiskā aizpilde un citas saistītās iespējas šajās tīmekļvietnēs netiks piedāvātas. Ir jāatsvaidzina lapa, lai izmaiņas iedarbotos." + }, + "autofillBlockedNoticeV2": { + "message": "Automātiskā aizpilde šajā tīmekļvietnē ir liegta." + }, + "autofillBlockedNoticeGuidance": { + "message": "To var mainīt iestatījumos" + }, "websiteItemLabel": { "message": "Tīmekļvietne $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Liegtā domēna vārda izmaiņas sglabātas" + }, "excludedDomainsSavedSuccess": { "message": "Saglabātas vērā neņemto domēna vārdu izmaiņas" }, @@ -2389,14 +2394,6 @@ "message": "Informācija par Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Meklēt Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Pievienot Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Teksts" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Pēc noklusējuma paslēpt tekstu" }, - "maxAccessCountReached": { - "message": "Sasniegts lielākais pieļaujamais piekļuves reižu skaits", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Beidzies izmantošanas laiks" }, - "pendingDeletion": { - "message": "Gaida dzēšanu" - }, "passwordProtected": { "message": "Aizsargāts ar paroli" }, @@ -2472,24 +2462,9 @@ "message": "Labot Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Kāds ir šī Send veids?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Lasāms nosaukums, kas apraksta šo Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Datne, kuru ir vēlme nosūtīt." - }, "deletionDate": { "message": "Dzēšanas datums" }, - "deletionDateDesc": { - "message": "Send tiks neatgriezeniski izdzēsts norādītajā datumā un laikā.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send šajā datumā tiks neatgriezeniski izdzēsts.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Derīguma beigu datums" }, - "expirationDateDesc": { - "message": "Ja iestatīts, piekļuve šim Send beigsies norādītajā datumā un laikā.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 diena" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Pielāgots" }, - "maximumAccessCount": { - "message": "Lielākais pieļaujamais piekļuves reižu skaits" - }, - "maximumAccessCountDesc": { - "message": "Ja iestatīts, lietotāji nevarēs piekļūt šim Send, kad tiks sasniegts lielākais pieļaujamais piekļūšanas reižu skaits.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Pēc izvēles pieprasīt paroli, lai lietotāji varētu piekļūt šim Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Pēc izvēles var pievienot paroli, lai saņēmēji varētu piekļūt šim Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Personīgas piezīmes par šo Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Izslēgt šo Send, lai neviens tam nevarētu piekļūt.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Saglabāšanas brīdī ievietot šī Send saiti starpliktuvē.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Teksts, kuru ir vēlme nosūtīt." - }, - "sendHideText": { - "message": "Pēc noklusējuma paslēpt šī Send tekstu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Pašreizējais piekļuvju skaits" - }, "createSend": { "message": "Jauns Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Pirms sākšanas" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Lai izmantotu kalendāra veida datumu atlasītāju,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klikšķināt šeit", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": ", lai atvērtu jaunā logā.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Norādītais derīguma beigu datums nav derīgs." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Atgadījusies kļūda dzēšanas un derīguma beigu datumu saglabāšanā." }, - "hideEmail": { - "message": "Slēpt e-pasta adresi no saņēmējiem." - }, "hideYourEmail": { "message": "Paslēpt e-pasta adresi no apskatītājiem." }, - "sendOptionsPolicyInEffect": { - "message": "Viens vai vairāki apvienības nosacījumi ietekmē Send iespējas." - }, "passwordPrompt": { "message": "Galvenās paroles pārvaicāšana" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Kļūda" }, - "regenerateUsername": { - "message": "Pārizveidot lietotājvārdu" + "decryptionError": { + "message": "Atšifrēšanas kļūda" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nevarēja atšifrēt zemāk uzskaitītos glabātavas vienumus." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Jāsazinās ar klientu atbalstu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "lai izvairītos no papildu datu zaudējumiem.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Izveidot lietotājvārdu" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Lietotājvārda veids" - }, "plusAddressedEmail": { "message": "E-pasta adrese ar plusu", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Tīmekļvietnes nosaukums" }, - "whatWouldYouLikeToGenerate": { - "message": "Ko ir nepieciešams izveidot?" - }, - "passwordType": { - "message": "Paroles veids" - }, "service": { "message": "Pakalpojums" }, @@ -3503,38 +3425,6 @@ "message": "Pārslēgt sakļaušanu", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Ievietot datus Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Aizsargāt LastPass datus un ievietot tos Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Saglabāt kā nešifrētu datni", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Ievietot Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Ievieto...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dati veiksmīgi ievietoti.", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Kļūda ievietošanā. Jāpārbauda konsole, lai iegūtu vairāk informācijas.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Ievietošanas laikā atgadījās tīkla kļūda.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aizstājdomēns" }, @@ -4086,7 +3976,10 @@ "message": "Piekļuves atslēga noņemta" }, "autofillSuggestions": { - "message": "Ieteikumi automātiskajai aizpildei" + "message": "Automātiskās aizpildes ieteikumi" + }, + "itemSuggestions": { + "message": "Ieteiktie vienumi" }, "autofillSuggestionsTip": { "message": "Saglabāt pieteikšanās vienumi, ko automātiski aizpildīt šajā vietnē" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Teksta Send" }, - "bitwardenNewLook": { - "message": "Bitwarden ir jauns izskats." - }, - "bitwardenNewLookDesc": { - "message": "Veikt automātisko aizpildi un meklēšanu glabātavas cilnē ir vienkāršāk un izprotamāk kā jebkad. Apskati izmaiņas!" - }, "accountActions": { "message": "Konta darbības" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Nav nepieciešamo atļauju, lai labotu šo vienumu" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama, jo vispirms ir nepieciešama atslēgšana ar PIN vai paroli." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Atslēgšana ar biometriju nav pieejama, jo Bitwarden darbvirsmas lietotne ir aizvērta." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Atslēgšana ar biometriju nav pieejama, jo tā nav iespējota $EMAIL$ Bitwarden darbvirsmas lietotnē.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama nezināma iemesla dēļ." + }, "authenticating": { "message": "Autentificē" }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index e3c377b2e8c..cfbb1972388 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -454,9 +454,6 @@ "length": { "message": "നീളം" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "കുറഞ്ഞ പ്രത്യേക പ്രതീകങ്ങൾ" }, - "avoidAmbChar": { - "message": "അവ്യക്തമായ പ്രതീകങ്ങൾ ഒഴിവാക്കുക", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "ക്ലിപ്ബോര്‍ഡ് മായ്ക്കുക", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "പങ്കിടുക" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "ക്ലോൺ" }, - "passwordGeneratorPolicyInEffect": { - "message": "ഒന്നോ അതിലധികമോ സംഘടന നയങ്ങൾ നിങ്ങളുടെ പാസ്സ്‌വേഡ് സൃഷ്ടാവിൻ്റെ ക്രമീകരണങ്ങളെ ബാധിക്കുന്നു" - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 02c66ea8f69..05d6034acec 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -454,9 +454,6 @@ "length": { "message": "लांबी" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 8f7673803cb..a7913bbfc9b 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 604597091ef..df676d2a3ba 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -20,19 +20,19 @@ "message": "Opprett en konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Er du ny til Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Logg inn med passnøkkel" }, "useSingleSignOn": { "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Velkommen tilbake" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Velg et sterkt passord" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -138,31 +138,31 @@ "message": "Kopier sikkerhetskoden" }, "copyName": { - "message": "Copy name" + "message": "Kopiér navn" }, "copyCompany": { "message": "Copy company" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopiér fødselsnummer" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiér passnummer" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopiér lisensnummer" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiér privat nøkkel" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiér offentlig nøkkel" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Kopiér fingeravtrykk" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiér $FIELD$", "placeholders": { "field": { "content": "$1", @@ -171,13 +171,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiér nettsted" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopiér notater" }, "fill": { - "message": "Fill", + "message": "Fyll", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "Auto-utfyll identitet" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Fyll inn verifiseringskode" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Fyll inn verifiseringskode", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -239,13 +239,13 @@ "message": "Legg til en gjenstand" }, "accountEmail": { - "message": "Account email" + "message": "Kontoens E-postadresse" }, "requestHint": { - "message": "Request hint" + "message": "Be om et hint" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Be om passordhint" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -316,7 +316,7 @@ "message": "Logg ut" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Om Bitwarden" }, "about": { "message": "Om" @@ -325,7 +325,7 @@ "message": "More from Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Vil du fortsette til bitwarden.com?" }, "bitwardenForBusiness": { "message": "Bitwarden for Business" @@ -382,7 +382,7 @@ "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "Ingen mapper er lagt til" }, "createFoldersToOrganize": { "message": "Create folders to organize your vault items" @@ -454,9 +454,6 @@ "length": { "message": "Lengde" }, - "passwordMinLength": { - "message": "Minimum passordlengde" - }, "uppercase": { "message": "Store bokstaver (A–Å)", "description": "deprecated. Use uppercaseLabel instead." @@ -478,7 +475,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Inkluder store bokstaver", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -486,7 +483,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Inkluder små bokstaver", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -502,7 +499,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Inkluder spesialtegn", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -528,12 +525,8 @@ "minSpecial": { "message": "Minste antall spesialtegn" }, - "avoidAmbChar": { - "message": "Unngå tvetydige tegn", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Unngå forvekslingsbare tegn", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -571,19 +564,19 @@ "message": "Favoritt" }, "unfavorite": { - "message": "Unfavorite" + "message": "Fjern favorittstempel" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Gjenstand lagt til i favorittene" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Gjenstand fjernet fra favorittene" }, "notes": { "message": "Notater" }, "privateNote": { - "message": "Private note" + "message": "Privat notat" }, "note": { "message": "Notat" @@ -604,10 +597,10 @@ "message": "Åpne" }, "launchWebsite": { - "message": "Launch website" + "message": "Åpne nettstedet" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Åpne nettstedet $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -664,7 +657,7 @@ "message": "Your account is locked" }, "or": { - "message": "or" + "message": "eller" }, "unlock": { "message": "Lås opp" @@ -752,7 +745,7 @@ "message": "Your master password cannot be recovered if you forget it!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Få et hint om hovedpassordet" }, "errorOccurred": { "message": "En feil har oppstått" @@ -792,10 +785,10 @@ "message": "You have been logged in!" }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Du har vellykket logget inn" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Du kan lukke dette vinduet" }, "masterPassSent": { "message": "Vi har sendt deg en E-post med hintet til superpassordet." @@ -841,10 +834,10 @@ "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." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Lær mer om autentisering" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Kopier autentiseringsnøkkel (TOTP)" }, "loggedOut": { "message": "Logget av" @@ -859,19 +852,19 @@ "message": "Logg inn" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Logg inn på Bitwarden" }, "restartRegistration": { "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Utløpt lenke" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Du har kanskje allerede en konto" }, "logOutConfirmation": { "message": "Er du sikker på at du vil logge av?" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Vis identitetselementer på fanesiden for enkel auto-utfylling." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Tøm utklippstavlen", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1053,7 +1049,7 @@ "message": "Lås opp" }, "additionalOptions": { - "message": "Additional options" + "message": "Ekstra innstillinger" }, "enableContextMenuItem": { "message": "Vis alternativer for kontekstmeny" @@ -1117,7 +1113,7 @@ "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "Eksporttype" }, "accountRestricted": { "message": "Account restricted" @@ -1130,7 +1126,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Advarsel", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Flytt til organisasjon" }, - "share": { - "message": "Del" - }, "movedItemToOrg": { "message": "$ITEMNAME$ flyttet til $ORGNAME$", "placeholders": { @@ -1213,7 +1206,7 @@ "message": "Fil" }, "fileToShare": { - "message": "File to share" + "message": "Filen som skal deles" }, "selectFile": { "message": "Velg en fil." @@ -1249,7 +1242,7 @@ "message": "1 GB med kryptert fillagring for filvedlegg." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Nødtilgang." }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." @@ -1420,7 +1413,7 @@ "message": "E-post" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Skriv inn koden du har fått tilsendt på E-post." }, "selfHostedEnvironment": { "message": "Selvbetjent miljø" @@ -1473,7 +1466,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Autoutfyllingsforslag" }, "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" @@ -1521,7 +1514,7 @@ "message": "Kompromitterte eller upålitelige nettsider kan utnytte auto-utfylling når du laster inn siden." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Lær mer om risikoer" }, "learnMoreAboutAutofill": { "message": "Lær mer om auto-utfylling" @@ -1775,10 +1768,10 @@ "message": "Identitet" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH-nøkkel" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Ny $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1787,7 +1780,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Rediger $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1796,7 +1789,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Vis $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1808,7 +1801,7 @@ "message": "Passordhistorikk" }, "generatorHistory": { - "message": "Generator history" + "message": "Generatorhistorikk" }, "clearGeneratorHistoryTitle": { "message": "Clear generator history" @@ -1853,7 +1846,7 @@ "message": "Sikre notiser" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH-nøkler" }, "clear": { "message": "Tøm", @@ -1936,7 +1929,7 @@ "message": "Tøm historikk" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Ingenting å vise" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -2024,7 +2017,7 @@ "message": "Lås opp med biometri" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Lås opp med hovedpassord" }, "awaitDesktop": { "message": "Venter på bekreftelse fra skrivebordsprogrammet" @@ -2036,7 +2029,7 @@ "message": "Lås med hovedpassordet når du starter nettleseren på nytt" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Krev hovedpassord ved omstart av nettleseren" }, "selectOneCollection": { "message": "Du må velge minst én samling." @@ -2047,26 +2040,23 @@ "clone": { "message": "Klon" }, - "passwordGeneratorPolicyInEffect": { - "message": "En eller flere av virksomhetens regler påvirker generatorinnstillingene dine." - }, "passwordGenerator": { - "message": "Password generator" + "message": "Passordgenerator" }, "usernameGenerator": { - "message": "Username generator" + "message": "Brukernavngenerator" }, "useThisPassword": { "message": "Bruk dette passordet" }, "useThisUsername": { - "message": "Use this username" + "message": "Bruk dette brukernavnet" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Bruk denne generatoren", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { @@ -2106,7 +2096,7 @@ "message": "Gjenopprettet objekt" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Har du allerede en konto?" }, "vaultTimeoutLogOutConfirmation": { "message": "Hvis du logger ut, fjerner du all tilgang til hvelvet ditt og krever online godkjenning etter tidsavbrudd. Er du sikker på at du vil bruke denne innstillingen?" @@ -2118,7 +2108,7 @@ "message": "Autofyll og lagre" }, "fillAndSave": { - "message": "Fill and save" + "message": "Fyll og lagre" }, "autoFillSuccessAndSavedUri": { "message": "Autoutfylt objekt og lagret URI" @@ -2205,10 +2195,10 @@ "message": "Avslutt abonnement" }, "atAnyTime": { - "message": "at any time." + "message": "når som helst." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Ved å fortsette, samtykker du til" }, "and": { "message": "og" @@ -2334,6 +2324,9 @@ "message": "Domener", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokkerte domener" + }, "excludedDomains": { "message": "Ekskluderte domener" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,11 +2363,14 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, "limitSendViews": { - "message": "Limit views" + "message": "Begrens visninger" }, "limitSendViewsHint": { "message": "No one can view this Send after the limit is reached.", @@ -2389,19 +2394,11 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Søk i Send-ene", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Legg til Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekst" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Teksten som skal deles" }, "sendTypeFile": { "message": "Fil" @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Maksimalt antall tilganger nådd", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Utløpt" }, - "pendingDeletion": { - "message": "Venter på sletting" - }, "passwordProtected": { "message": "Passord beskyttet" }, @@ -2472,24 +2462,9 @@ "message": "Rediger Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Hvilken type Send er dette?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Et vennlig navn for å beskrive dette Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Filen du vil send." - }, "deletionDate": { "message": "Dato for sletting" }, - "deletionDateDesc": { - "message": "Send-en vil bli slettet permanent på den angitte dato og klokkeslett.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Utløpsdato" }, - "expirationDateDesc": { - "message": "Hvis satt, vil tilgang til denne Send gå ut på angitt dato og klokkeslett.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dag" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Egendefinert" }, - "maximumAccessCount": { - "message": "Maksimal antall tilganger" - }, - "maximumAccessCountDesc": { - "message": "Hvis satt, vil ikke brukere lenger ha tilgang til dette Send når maksimal antall tilgang er nådd.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Eventuelt krever et passord for brukere å få tilgang til denne Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notater om denne Send-en.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deaktiver denne Send-en, slik at ingen får tilgang til den.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopier denne Send-ens lenke til utklippstavlen når den har blitt lagret.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Teksten du ønsker å sende." - }, - "sendHideText": { - "message": "Skjul denne Send-ens tekst som standard.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Antall nåværende tilganger" - }, "createSend": { "message": "Lag en ny Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Før du starter" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Hvis du vil bruke en kalenderstil-datovelger", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "kilkk her", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "å pope ut vinduet.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Utløpsdatoen angitt er ikke gyldig." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Det oppstod en feil ved lagring av slettingen og utløpsdatoene." }, - "hideEmail": { - "message": "Skjul min e-postadresse fra mottakere." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "En eller flere av virksomhetens regler påvirker generatorinnstillingene dine." - }, "passwordPrompt": { "message": "Forespørsel om hovedpassord på nytt" }, @@ -2714,7 +2634,7 @@ "message": "Velg mappe …" }, "noFoldersFound": { - "message": "No folders found", + "message": "Ingen mapper ble funnet", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2726,7 +2646,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "av $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2884,8 +2804,19 @@ "error": { "message": "Feil" }, - "regenerateUsername": { - "message": "Regenerer brukernavn" + "decryptionError": { + "message": "Dekrypteringsfeil" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generer brukernavn" @@ -2894,7 +2825,7 @@ "message": "Generate email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Verdien må være mellom $MIN$ og $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Brukernavntype" - }, "plusAddressedEmail": { "message": "Pluss-adressert e-post", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Navn på nettside" }, - "whatWouldYouLikeToGenerate": { - "message": "Hva vil du generere?" - }, - "passwordType": { - "message": "Passordtype" - }, "service": { "message": "Tjeneste" }, @@ -2968,7 +2890,7 @@ "message": "Generer et e-postalias med en ekstern videresendingstjeneste." }, "forwarderDomainName": { - "message": "Email domain", + "message": "E-postdomene", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -2990,11 +2912,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Nettsted: $WEBSITE$. Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -3038,7 +2960,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Ugyldig $SERVICENAME$-domene.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3175,16 +3097,16 @@ "message": "Send varslingen på nytt" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Vis alle påloggingsalternativer" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Vis alle påloggingsalternativer" }, "notificationSentDevice": { "message": "Et varsel er sendt til enheten din." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Et varsel ble sendt til enheten din" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" @@ -3193,7 +3115,7 @@ "message": "You will be notified once the request is approved" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Trenger du et annet alternativ?" }, "loginInitiated": { "message": "Login initiated" @@ -3295,7 +3217,7 @@ "message": "Device approval required. Select an approval option below:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Enhetsgodkjennelse kreves" }, "selectAnApprovalOptionBelow": { "message": "Select an approval option below" @@ -3319,7 +3241,7 @@ "message": "Organization SSO identifier is required." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Oppretter en konto på" }, "checkYourEmail": { "message": "Check your email" @@ -3337,7 +3259,7 @@ "message": "Gå tilbake" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "for å redigere E-postadressen din." }, "eu": { "message": "EU", @@ -3365,7 +3287,7 @@ "message": "Du vil bli varslet når det er godkjent." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Har du problemer med å logge inn?" }, "loginApproved": { "message": "Innlogging godkjent" @@ -3377,7 +3299,7 @@ "message": "Active user email not found. Logging you out." }, "deviceTrusted": { - "message": "Device trusted" + "message": "Enheten er betrodd" }, "sendsNoItemsTitle": { "message": "No active Sends", @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Vil du importere dataene dine til Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Lagre som ukryptert fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importer til Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerer …", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dataene ble vellykket importert!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias-domene" }, @@ -3565,7 +3455,7 @@ "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "Bitwardens autoutfyllingsmeny", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { @@ -3621,7 +3511,7 @@ "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "Nytt kort", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { @@ -3651,7 +3541,7 @@ "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Importeringsfeil" }, "importErrorDesc": { "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." @@ -3815,16 +3705,16 @@ "message": "Vault data exported" }, "typePasskey": { - "message": "Passkey" + "message": "Passnøkkel" }, "accessing": { "message": "Accessing" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Innlogget!" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Passkoden vil ikke bli kopiert" }, "passkeyNotCopiedAlert": { "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" @@ -3854,10 +3744,10 @@ "message": "Bekreft" }, "savePasskey": { - "message": "Save passkey" + "message": "Lagre passnøkkel" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Lagre passnøkkelen som en ny pålogging" }, "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" @@ -3920,7 +3810,7 @@ "message": "Approve the login request in your authentication app or enter a one-time passcode." }, "passcode": { - "message": "Passcode" + "message": "Passkode" }, "lastPassMasterPassword": { "message": "LastPass-hovedpassord" @@ -3960,7 +3850,7 @@ "message": "Bytt kontoer" }, "switchToAccount": { - "message": "Switch to account" + "message": "Bytt til konto" }, "activeAccount": { "message": "Aktiv konto" @@ -3981,13 +3871,13 @@ "message": "låst opp" }, "server": { - "message": "server" + "message": "tjener" }, "hostedAt": { "message": "betjent hos" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "Bruk enhets- eller maskinvarenøkkel" }, "justOnce": { "message": "Kun én gang" @@ -4009,7 +3899,7 @@ "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Vil du fortsette til nettleserinnstillingene?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { @@ -4053,7 +3943,7 @@ "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Gjør det til standarden", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { @@ -4061,7 +3951,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Passordet ble lagret!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -4069,7 +3959,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Passordet ble oppdatert!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -4080,7 +3970,7 @@ "message": "Suksess" }, "removePasskey": { - "message": "Remove passkey" + "message": "Fjern passordnøkkel" }, "passkeyRemoved": { "message": "Passkey removed" @@ -4088,11 +3978,14 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Hvelvet ditt er tomt" }, "noItemsMatchSearch": { "message": "No items match your search" @@ -4121,7 +4014,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Flere innstillinger, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4131,7 +4024,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Flere innstillinger - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4141,7 +4034,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Vis gjenstand - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4151,7 +4044,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "Autoutfyll - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4164,7 +4057,7 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Legg til i samlinger" }, "copyEmail": { "message": "Copy email" @@ -4194,7 +4087,7 @@ "message": "Error assigning target folder." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Vis gjenstander i $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4204,7 +4097,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Tilbake til $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4217,7 +4110,7 @@ "message": "Ny" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Fjern $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4227,13 +4120,13 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Gjenstander uten mappe" }, "itemDetails": { - "message": "Item details" + "message": "Gjenstandens detaljer" }, "itemName": { - "message": "Item name" + "message": "Gjenstandens navn" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -4261,10 +4154,10 @@ "message": "Tilleggsinformasjon" }, "itemHistory": { - "message": "Item history" + "message": "Gjenstandshistorikk" }, "lastEdited": { - "message": "Last edited" + "message": "Nyligst redigert" }, "ownerYou": { "message": "Owner: You" @@ -4279,10 +4172,10 @@ "message": "Last opp" }, "addAttachment": { - "message": "Add attachment" + "message": "Legg til vedlegg" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Maksimal filstørrelse er 500 MB" }, "deleteAttachmentName": { "message": "Delete attachment $NAME$", @@ -4294,7 +4187,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Last ned $NAME$", "placeholders": { "name": { "content": "$1", @@ -4318,10 +4211,10 @@ "message": "Filter vault" }, "filterApplied": { - "message": "One filter applied" + "message": "Ett filter er benyttet" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ filtre er benyttet", "placeholders": { "count": { "content": "$1", @@ -4330,16 +4223,16 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "Personlige detaljer" }, "identification": { - "message": "Identification" + "message": "Identifikasjon" }, "contactInfo": { "message": "Kontaktinfo" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "Last ned - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4348,17 +4241,17 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "kortnummeret slutter med", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { "message": "Legitimasjoner for innlogging" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Autentiseringsnøkkel" }, "autofillOptions": { - "message": "Autofill options" + "message": "Autoutfyllings-innstillinger" }, "websiteUri": { "message": "Website (URI)" @@ -4374,7 +4267,7 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Nettsted lagt til" }, "addWebsite": { "message": "Legg til nettsted" @@ -4383,7 +4276,7 @@ "message": "Slett nettsted" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Standard ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4414,16 +4307,16 @@ "message": "Autofill on page load?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Utløpt kort" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "Kortdetaljer" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$-detaljer", "placeholders": { "brand": { "content": "$1", @@ -4435,7 +4328,7 @@ "message": "Aktiver animasjoner" }, "showAnimations": { - "message": "Show animations" + "message": "Vis animasjoner" }, "addAccount": { "message": "Legg til konto" @@ -4447,15 +4340,15 @@ "message": "Data" }, "passkeys": { - "message": "Passkeys", + "message": "Passnøkler", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Passord", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "Logg inn med passnøkkel", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4480,16 +4373,16 @@ } }, "addField": { - "message": "Add field" + "message": "Legg til felt" }, "add": { "message": "Legg til" }, "fieldType": { - "message": "Field type" + "message": "Felttype" }, "fieldLabel": { - "message": "Field label" + "message": "Feltetikett" }, "textHelpText": { "message": "Use text fields for data like security questions" @@ -4519,7 +4412,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "Slett $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4528,7 +4421,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ er lagt til", "placeholders": { "label": { "content": "$1", @@ -4650,7 +4543,7 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "Gjenstandens plassering" }, "fileSend": { "message": "File Send" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4686,16 +4573,16 @@ "message": "Enterprise policy requirements have been applied to this setting" }, "sshPrivateKey": { - "message": "Private key" + "message": "Privat nøkkel" }, "sshPublicKey": { - "message": "Public key" + "message": "Offentlig nøkkel" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Fingeravtrykk" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Nøkkeltype" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4716,22 +4603,22 @@ "message": "Minimum custom timeout is 1 minute." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "Ytterligere innhold er tilgjengelig" }, "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." }, "showCharacterCount": { - "message": "Show character count" + "message": "Vis tegntelleren" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "Skjul tegntelleren" }, "itemsInTrash": { - "message": "Items in trash" + "message": "Gjenstander i papirkurven" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "Ingen gjenstander i papirkurven" }, "noItemsInTrashDesc": { "message": "Items you delete will appear here and be permanently deleted after 30 days" @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autentiserer" }, @@ -4760,11 +4674,11 @@ "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Vil du lagre påloggingen i Bitwarden?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Mellomrom", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { @@ -4772,15 +4686,15 @@ "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Baklengs apostrof", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Utropstegn", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Alfakrøll", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { @@ -4788,11 +4702,11 @@ "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Dollartegn", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Prosenttegn", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { @@ -4800,7 +4714,7 @@ "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Prosenttegn", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { @@ -4808,23 +4722,23 @@ "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Venstre parantes", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Høyre parantes", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Understrek", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Bindestrek", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Plusstegn", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { @@ -4832,19 +4746,19 @@ "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Venstre krøllparentes", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Høyre krøllparentes", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Venstre firkantparantes", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Høyre firkantparantes", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { @@ -4852,69 +4766,69 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Skråstrek bakover", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Kolon", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Semikolon", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Hermetegn", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Apostrofe", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Mindre enn", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Større enn", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Komma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Tidsperiode", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Spørsmålstegn", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Skråstrek", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Små bokstaver" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Store bokstaver" }, "generatedPassword": { - "message": "Generated password" + "message": "Generert passord" }, "compactMode": { - "message": "Compact mode" + "message": "Kompakt modus" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Viktig melding" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Sett opp 2-trinnspålogging" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." @@ -4935,7 +4849,7 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nei, det gjør jeg ikke" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { "message": "Yes, I can reliably access my email" @@ -4944,15 +4858,15 @@ "message": "Turn on two-step login" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Endre kontoens E-postadresse" }, "extensionWidth": { - "message": "Extension width" + "message": "Utvidelsens bredde" }, "wide": { - "message": "Wide" + "message": "Bred" }, "extraWide": { - "message": "Extra wide" + "message": "Ekstra bred" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 8f7673803cb..a7913bbfc9b 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 170c682aa1c..8a70ea4548c 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Thuis, op werk of onderweg. Bitwarden beveiligt makkelijk all je wachtwoorden, passkeys en gevoelige informatie", + "message": "Thuis, op werk of onderweg. Bitwarden beveiligt makkelijk al je wachtwoorden, passkeys en gevoelige informatie", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -38,7 +38,7 @@ "message": "Rond het aanmaken van je account af met het instellen van een wachtwoord" }, "enterpriseSingleSignOn": { - "message": "Single sign-on voor bedrijven" + "message": "Enterprise Single Sign-On" }, "cancel": { "message": "Annuleren" @@ -181,7 +181,7 @@ "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { - "message": "Auto-invullen" + "message": "Automatisch invullen" }, "autoFillLogin": { "message": "Login automatisch invullen" @@ -409,7 +409,7 @@ "message": "Ontdek Bitwarden community-forums" }, "contactSupport": { - "message": "Contacteer Bitwarden support" + "message": "Contacteer Bitwarden ondersteuning" }, "sync": { "message": "Synchroniseren" @@ -454,9 +454,6 @@ "length": { "message": "Lengte" }, - "passwordMinLength": { - "message": "Minimale wachtwoordlengte" - }, "uppercase": { "message": "Hoofdletters (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum aantal speciale tekens" }, - "avoidAmbChar": { - "message": "Dubbelzinnige tekens vermijden", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Dubbelzinnige tekens vermijden", "description": "Label for the avoid ambiguous characters checkbox." @@ -695,7 +688,7 @@ "message": "Nu vergrendelen" }, "lockAll": { - "message": "Vergrendel alles" + "message": "Alles vergrendelen" }, "immediately": { "message": "Onmiddellijk" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Identiteiten weergeven op de tabpagina voor gemakkelijk automatisch invullen." }, + "clickToAutofillOnVault": { + "message": "Klik op items om automatisch in te vullen op de kluisweergave" + }, "clearClipboard": { "message": "Klembord wissen", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1047,7 +1043,7 @@ "message": "Ja, nu bijwerken" }, "notificationUnlockDesc": { - "message": "Ontgrendel je Bitwarden-kluis om het auto-invulverzoek te voltooien." + "message": "Ontgrendel je Bitwarden-kluis om het automatisch invullen verzoek te voltooien." }, "notificationUnlock": { "message": "Ontgrendelen" @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Naar organisatie verplaatsen" }, - "share": { - "message": "Delen" - }, "movedItemToOrg": { "message": "$ITEMNAME$ verplaatst naar $ORGNAME$", "placeholders": { @@ -1207,7 +1200,7 @@ "message": "Geen bijlagen." }, "attachmentSaved": { - "message": "De bijlage is opgeslagen." + "message": "Bijlage opgeslagen" }, "file": { "message": "Bestand" @@ -1216,7 +1209,7 @@ "message": "Bestand om te delen" }, "selectFile": { - "message": "Selecteer een bestand." + "message": "Selecteer een bestand" }, "maxFileSize": { "message": "Maximale bestandsgrootte is 500 MB." @@ -1249,7 +1242,7 @@ "message": "1 GB versleutelde opslag voor bijlagen." }, "premiumSignUpEmergency": { - "message": "Noodtoegang" + "message": "Noodtoegang." }, "premiumSignUpTwoStepOptions": { "message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo." @@ -1432,10 +1425,10 @@ "message": "Specificeer de basis-URL van je zelfgehoste Bitwarden-installatie. Bijvoorbeeld: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Voor geavanceerde configuratie kun je de basis URL van elke service onafhankelijk opgeven." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Je moet de basis URL van de server of ten minste één aangepaste omgeving toevoegen." }, "customEnvironment": { "message": "Aangepaste omgeving" @@ -1466,14 +1459,14 @@ "message": "Pictogrammenserver-URL" }, "environmentSaved": { - "message": "De omgeving-URL's zijn opgeslagen." + "message": "Omgeving-URL's opgeslagen" }, "showAutoFillMenuOnFormFields": { - "message": "Auto-invulmenu op formuliervelden weergeven", + "message": "Automatisch invullen menu op formuliervelden weergeven", "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Suggesties voor automatisch invullen" + "message": "Suggesties automatisch invullen" }, "showInlineMenuLabel": { "message": "Suggesties voor automatisch invullen op formuliervelden weergeven" @@ -1518,7 +1511,7 @@ "message": "Als een inlogformulier wordt gedetecteerd, dan worden de inloggegevens automatisch ingevuld." }, "experimentalFeature": { - "message": "Gehackte of onbetrouwbare websites kunnen auto-invullen tijdens het laden van de pagina misbruiken." + "message": "Gehackte of onbetrouwbare websites kunnen automatisch invullen tijdens het laden van de pagina misbruiken." }, "learnMoreAboutAutofillOnPageLoadLinkText": { "message": "Meer over risico's lezen" @@ -1542,7 +1535,7 @@ "message": "Automatisch invullen bij laden van pagina" }, "autoFillOnPageLoadNo": { - "message": "Niet Automatisch invullen bij laden van pagina" + "message": "Niet automatisch invullen bij laden van pagina" }, "commandOpenPopup": { "message": "Open kluis in pop-up" @@ -1613,7 +1606,7 @@ "message": "Een herkenbare afbeelding naast iedere login weergeven." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Toon een herkenbare afbeelding naast elke login. Geldt voor alle ingelogde accounts." }, "enableBadgeCounter": { "message": "Teller weergeven" @@ -1697,7 +1690,7 @@ "message": "Dr." }, "mx": { - "message": "Mx" + "message": "Mx." }, "firstName": { "message": "Voornaam" @@ -2000,10 +1993,10 @@ "message": "Ontgrendelen met PIN" }, "setYourPinTitle": { - "message": "PIN-code instellen" + "message": "PIN instellen" }, "setYourPinButton": { - "message": "PIN-code instellen" + "message": "PIN instellen" }, "setYourPinCode": { "message": "Stel je PIN-code in voor het ontgrendelen van Bitwarden. Je PIN-code wordt opnieuw ingesteld als je je ooit volledig afmeldt bij de applicatie." @@ -2047,9 +2040,6 @@ "clone": { "message": "Dupliceren" }, - "passwordGeneratorPolicyInEffect": { - "message": "Een of meer organisatiebeleidseisen heeft invloed op de instellingen van je generator." - }, "passwordGenerator": { "message": "Wachtwoordgenerator" }, @@ -2121,10 +2111,10 @@ "message": "Invullen en opslaan" }, "autoFillSuccessAndSavedUri": { - "message": "Automatisch gevuld item en opgeslagen URI" + "message": "Automatisch ingevuld item en opgeslagen URI" }, "autoFillSuccess": { - "message": "Automatisch gevuld item" + "message": "Item automatisch ingevuld " }, "insecurePageWarning": { "message": "Waarschuwing: Dit is een onbeveiligde HTTP-pagina waardoor anderen alle informatie die je verstuurt kunnen zien en wijzigen. Deze login is oorspronkelijk opgeslagen op een beveiligde (HTTPS) pagina." @@ -2235,7 +2225,7 @@ "message": "Fout bij vernieuwen toegangstoken" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Geen verversingstoken of API-sleutels gevonden. Probeer uit te loggen en weer in te loggen." }, "desktopSyncVerificationTitle": { "message": "Desktopsynchronisatieverificatie" @@ -2292,10 +2282,10 @@ "message": "Dit apparaat ondersteunt geen browserbiometrie." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "Gebruiker vergrendeld of uitgelogd" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "Ontgrendel deze gebruiker in de desktopapplicatie en probeer het opnieuw." }, "biometricsNotAvailableTitle": { "message": "Biometrisch ontgrendelen niet beschikbaar" @@ -2334,6 +2324,9 @@ "message": "Domeinen", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Geblokkeerde domeinen" + }, "excludedDomains": { "message": "Uitgesloten domeinen" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden zal voor deze domeinen niet vragen om de wachtwoorden op te slaan voor alle ingelogde accounts. Je moet de pagina verversen om de wijzigingen op te slaan." }, + "blockedDomainsDesc": { + "message": "Autofill en andere gerelateerde functies worden niet aangeboden voor deze websites. Vernieuw de pagina om de wijzigingen toe te passen." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is geblokkeerd voor deze website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Dit aanpassen in instellingen" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Wijzigingen in geblokkeerde domeinen opgeslagen" + }, "excludedDomainsSavedSuccess": { "message": "Uitgesloten domeinwijzigingen opgeslagen" }, @@ -2389,14 +2394,6 @@ "message": "Send-details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Sends zoeken", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Send toevoegen", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekst" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Tekst standaard verbergen" }, - "maxAccessCountReached": { - "message": "Maximum aantal keren benaderd", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Verlopen" }, - "pendingDeletion": { - "message": "Wordt verwijderd" - }, "passwordProtected": { "message": "Beveiligd met wachtwoord" }, @@ -2472,24 +2462,9 @@ "message": "Send bewerken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Wat voor soort Send is dit?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Een vriendelijke naam om deze Send te beschrijven.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Het bestand dat je wilt versturen." - }, "deletionDate": { "message": "Verwijderingsdatum" }, - "deletionDateDesc": { - "message": "Deze Send wordt op de aangegeven datum en tijd definitief verwijderd.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Op deze datum wordt de Send definitief verwijderd.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Vervaldatum" }, - "expirationDateDesc": { - "message": "Als dit is ingesteld verloopt deze Send op een specifieke datum en tijd.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dag" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Aangepast" }, - "maximumAccessCount": { - "message": "Maximum toegangsaantal" - }, - "maximumAccessCountDesc": { - "message": "Als dit is ingesteld kunnen gebruikers deze Send niet meer benaderen zodra het maximale aantal toegang is bereikt.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Vereis optioneel een wachtwoord voor gebruikers om toegang te krijgen tot deze Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Voeg een optioneel wachtwoord toe voor ontvangers om toegang te krijgen tot deze Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Privénotities over deze Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Schakel deze Send uit zodat niemand hem kan benaderen.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopieer de link van deze Send bij het opslaan naar het klembord.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "De tekst die je wilt versturen." - }, - "sendHideText": { - "message": "De tekst van deze Send standaard verbergen.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Huidige toegangsaantal" - }, "createSend": { "message": "Nieuwe Send aanmaken", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Voor je begint" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Om een datumkiezer in kalenderstijl te gebruiken", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klik hier", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "om een pop-up te openen.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "De opgegeven vervaldatum is niet geldig." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Er is een fout opgetreden bij het opslaan van je verwijder- en vervaldatum." }, - "hideEmail": { - "message": "Verberg mijn e-mailadres voor ontvangers." - }, "hideYourEmail": { "message": "Je e-mailadres voor ontvangers verbergen." }, - "sendOptionsPolicyInEffect": { - "message": "Een of meer organisatiebeleidseisen heeft invloed op de mogelijkheden van je Send." - }, "passwordPrompt": { "message": "Hoofdwachtwoord opnieuw vragen" }, @@ -2718,11 +2638,11 @@ "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "De machtigingen van je organisatie zijn bijgewerkt, waardoor je een hoofdwachtwoord moet instellen.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Je organisatie vereist dat je een hoofdwachtwoord instelt.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { @@ -2861,7 +2781,7 @@ "message": "Persoonlijke kluis exporteren" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Alleen de individuele kluisitems die gekoppeld zijn aan $EMAIL$ worden geëxporteerd. Kluisitems van organisaties worden niet meegenomen. Alleen de informatie over het kluisitem wordt geëxporteerd en niet de bijbehorende bijlagen.", "placeholders": { "email": { "content": "$1", @@ -2884,8 +2804,19 @@ "error": { "message": "Fout" }, - "regenerateUsername": { - "message": "Gebruikersnaam opnieuw genereren" + "decryptionError": { + "message": "Ontsleutelingsfout" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kon de onderstaande kluisitem(s) niet ontsleutelen." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Neem contact op met de klantenservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "om extra dataverlies te voorkomen.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Gebruikersnamen genereren" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Type gebruikersnaam" - }, "plusAddressedEmail": { "message": "E-mailadres-met-plus", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Websitenaam" }, - "whatWouldYouLikeToGenerate": { - "message": "Wat wil je genereren?" - }, - "passwordType": { - "message": "Type wachtwoord" - }, "service": { "message": "Dienst" }, @@ -2976,7 +2898,7 @@ "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ fout: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2994,7 +2916,7 @@ "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Website: $WEBSITE$. Gegenereerd door Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -3004,7 +2926,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Ongeldig $SERVICENAME$ API token", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3014,7 +2936,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Ongeldige $SERVICENAME$ API token: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3028,7 +2950,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Kan $SERVICENAME$ gemaskeerde e-mailaccount-ID niet verkrijgen.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3038,7 +2960,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Ongeldig $SERVICENAME$ domein.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3048,7 +2970,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Ongeldige $SERVICENAME$ url.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -3058,7 +2980,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Onbekende $SERVICENAME$ fout opgetreden.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3068,7 +2990,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Onbekende doorstuurder: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3124,7 +3046,7 @@ "message": "zelfgehost" }, "thirdParty": { - "message": "van derden" + "message": "Derde partij" }, "thirdPartyServerMessage": { "message": "Verbonden met server implementatie van derden, $SERVERNAME$. Reproduceer bugs via de officiële server, of meld ze bij het serverproject.", @@ -3178,7 +3100,7 @@ "message": "Alle inlogopties bekijken" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Alle inlogopties weergeven" }, "notificationSentDevice": { "message": "Er is een melding naar je apparaat verzonden." @@ -3232,10 +3154,10 @@ "message": "Je organisatiebeleid heeft het automatisch invullen bij laden van pagina ingeschakeld." }, "howToAutofill": { - "message": "Hoe automatisch aanvullen" + "message": "Hoe automatisch invullen" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "Selecteer een item in dit scherm, gebruik de sneltoets $COMMAND$ of verken andere opties in de instellingen.", "placeholders": { "command": { "content": "$1", @@ -3244,16 +3166,16 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "Selecteer een item in dit scherm of verken andere opties in de instellingen." }, "gotIt": { - "message": "Ik snap het" + "message": "Oké" }, "autofillSettings": { "message": "Instellingen automatisch invullen" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Shortcut voor automatisch invullen" + "message": "Snelkoppeling automatisch invullen" }, "autofillKeyboardShortcutUpdateLabel": { "message": "Snelkoppeling wijzigen" @@ -3350,7 +3272,7 @@ "message": "Algemeen" }, "display": { - "message": "Display" + "message": "Weergave" }, "accountSuccessfullyCreated": { "message": "Account succesvol aangemaakt!" @@ -3479,7 +3401,7 @@ "message": "-- Type om te filteren --" }, "multiSelectLoading": { - "message": "Opties ophalen..." + "message": "Opties ophalen…" }, "multiSelectNotFound": { "message": "Geen items gevonden" @@ -3503,69 +3425,37 @@ "message": "In-/Uitklappen", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Gegevens importeren naar Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Je LastPass-gegevens beschermen en naar Bitwarden importeren?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Als niet-versleuteld bestand opslaan", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Naar Bitwarden importeren", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importeren...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Gegevens succesvol geïmporteerd!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fout bij importeren. Controleer console voor meer informatie.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Netwerkfout opgetreden tijdens het importeren.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomein" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "message": "Items met hoofdwachtwoord re-prompt kunnen niet automatisch worden ingevuld bij het laden van de pagina. Automatisch invullen bij laden van pagina uitgeschakeld.", "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Autofill on page load set to use default setting.", + "message": "Automatisch invullen bij het laden van een pagina ingesteld op de standaardinstelling.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Turn off master password re-prompt to edit this field", + "message": "Hoofdwachtwoord herhaalprompt uitschakelen om dit veld te bewerken", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { "message": "Zijnavigatie schakelen" }, "skipToContent": { - "message": "Skip to content" + "message": "Overslaan naar inhoud" }, "bitwardenOverlayButton": { "message": "Menuknop Bitwarden automatisch invullen", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Bitwarden auto-invulmenu in- en uitschakelen", + "message": "Bitwarden automatisch invullen menu in- en uitschakelen", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-invulmenu", + "message": "Bitwarden automatisch invullen menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { @@ -3593,7 +3483,7 @@ "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { - "message": "Inloggegevens invullen voor", + "message": "Referenties invullen voor", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { @@ -3637,7 +3527,7 @@ "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-invulmenu beschikbaar. Druk op de pijltjestoets omlaag om te selecteren.", + "message": "Bitwarden automatisch invullen menu beschikbaar. Druk op de pijltjestoets omlaag om te selecteren.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3681,7 +3571,7 @@ "message": "Deze actie vereist verificatie actie. Stel een pincode in om door te gaan." }, "setPin": { - "message": "Pincode instellen" + "message": "PIN instellen" }, "verifyWithBiometrics": { "message": "Biometrisch ontgrendelen" @@ -3726,7 +3616,7 @@ "message": "Fout bij het verbinden met de Duo-service. Gebruik een andere tweestapsaanmeldingsmethode of neem contact op met Duo voor hulp." }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Start Duo en volg de stappen om het inloggen te voltooien." }, "duoRequiredForAccount": { "message": "Jouw account vereist Duo-tweestapsaanmelding." @@ -3735,10 +3625,10 @@ "message": "Open de extensie om in te loggen." }, "popoutExtension": { - "message": "Popout extension" + "message": "Pop-out extensie" }, "launchDuo": { - "message": "Launch Duo" + "message": "Duo starten" }, "importFormatError": { "message": "De gegevens zijn niet correct opgemaakt. Controleer je importbestand en probeer het opnieuw." @@ -3866,7 +3756,7 @@ "message": "Kies een passkey om mee in te loggen" }, "passkeyItem": { - "message": "Passkey-Item" + "message": "Passkey item" }, "overwritePasskey": { "message": "Passkey overschrijven?" @@ -3908,7 +3798,7 @@ "message": "LastPass Email" }, "importingYourAccount": { - "message": "Account impoteren..." + "message": "Account impoteren…" }, "lastPassMFARequired": { "message": "LastPass multifactor-authenticatie vereist" @@ -3932,10 +3822,10 @@ "message": "Wacht op SSO-authenticatie" }, "awaitingSSODesc": { - "message": "Ga door met inloggen met de inloggegevens van je bedrijf." + "message": "Ga door met inloggen met de referenties van je bedrijf." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Zie gedetailleerde instructies op onze helpsite op", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { @@ -3951,52 +3841,52 @@ "message": "Collectie" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Steek de YubiKey die bij je LastPass account hoort in de USB poort van je computer en druk dan op de knop." }, "switchAccount": { - "message": "Switch account" + "message": "Account wisselen" }, "switchAccounts": { - "message": "Switch accounts" + "message": "Accounts wisselen" }, "switchToAccount": { - "message": "Switch to account" + "message": "Wisselen naar account" }, "activeAccount": { - "message": "Active account" + "message": "Actief account" }, "availableAccounts": { "message": "Beschikbare accounts" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "Accountlimiet bereikt. Log uit bij een account om een andere toe te voegen." }, "active": { - "message": "active" + "message": "actief" }, "locked": { - "message": "locked" + "message": "vergrendeld" }, "unlocked": { - "message": "unlocked" + "message": "ontgrendeld" }, "server": { "message": "server" }, "hostedAt": { - "message": "hosted at" + "message": "gehost op" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "Gebruik je apparaat of hardwaresleutel" }, "justOnce": { - "message": "Just once" + "message": "Eenmalig" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "Altijd voor deze site" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "$DOMAIN$ toegevoegd aan uitgesloten domeinen.", "placeholders": { "domain": { "content": "$1", @@ -4057,7 +3947,7 @@ "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Referenties succesvol opgeslagen!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { @@ -4065,7 +3955,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Referenties succesvol bijgewerkt!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { @@ -4073,7 +3963,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Fout bij het opslaan van referenties. Controleer console voor details.", "description": "Notification message for when saving credentials has failed." }, "success": { @@ -4086,7 +3976,10 @@ "message": "Passkey verwijderd" }, "autofillSuggestions": { - "message": "Suggesties voor automatisch invullen" + "message": "Suggesties automatisch invullen" + }, + "itemSuggestions": { + "message": "Voorgestelde items" }, "autofillSuggestionsTip": { "message": "Inlogitem opslaan voor automatisch invullen op deze site" @@ -4101,7 +3994,7 @@ "message": "Wis filters of probeer een andere zoekterm" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Info kopiëren - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4111,7 +4004,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "Notitie kopiëren - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4121,7 +4014,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Meer opties, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4131,7 +4024,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Meer opties - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4141,7 +4034,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Item bekijken - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4167,22 +4060,22 @@ "message": "Aan collecties toewijzen" }, "copyEmail": { - "message": "Copy email" + "message": "E-mail kopiëren" }, "copyPhone": { - "message": "Copy phone" + "message": "Telefoon kopiëren" }, "copyAddress": { - "message": "Copy address" + "message": "Adres kopiëren" }, "adminConsole": { - "message": "Admin Console" + "message": "Beheerconsole" }, "accountSecurity": { "message": "Accountbeveiliging" }, "notifications": { - "message": "Notifications" + "message": "Meldingen" }, "appearance": { "message": "Uiterlijk" @@ -4214,7 +4107,7 @@ } }, "new": { - "message": "New" + "message": "Nieuw" }, "removeItem": { "message": "Verwijder $NAME$", @@ -4352,7 +4245,7 @@ "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Inloggegevens" + "message": "Login referenties" }, "authenticatorKey": { "message": "Authenticatiesleutel" @@ -4653,10 +4546,10 @@ "message": "Itemlocatie" }, "fileSend": { - "message": "Bestand-Sends" + "message": "Bestand verzenden" }, "fileSends": { - "message": "Bestand-Sends" + "message": "Bestanden verzenden" }, "textSend": { "message": "Tekst-Sends" @@ -4664,17 +4557,11 @@ "textSends": { "message": "Tekst-Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden heeft een nieuw uiterlijk!" - }, - "bitwardenNewLookDesc": { - "message": "Automatisch invullen en zoeken is makkelijker en intuïtiever dan ooit vanaf het tabblad Kluis. Kijk rond!" - }, "accountActions": { "message": "Accountacties" }, "showNumberOfAutofillSuggestions": { - "message": "Aantal login-autofill-suggesties op het extensie-pictogram weergeven" + "message": "Aantal login automatisch invullen suggesties op het extensie-pictogram weergeven" }, "showQuickCopyActions": { "message": "Toon snelle kopieeracties in de kluis" @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Je hebt geen toestemming om dit item te bewerken" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat pincode of wachtwoordontgrendeling eerst vereist is." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat de Bitwarden-desktopapp is afgesloten." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat het niet is ingeschakeld voor $EMAIL$ in de Bitwarden-desktopapp.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar om een onbekende reden." + }, "authenticating": { "message": "Aan het inloggen" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 8f7673803cb..a7913bbfc9b 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 8f7673803cb..a7913bbfc9b 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 7025862cb72..b79acf95a1c 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Długość" }, - "passwordMinLength": { - "message": "Minimalna długość hasła" - }, "uppercase": { "message": "Wielkie litery (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimalna liczba znaków specjalnych" }, - "avoidAmbChar": { - "message": "Unikaj niejednoznacznych znaków", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Unikaj niejednoznacznych znaków", "description": "Label for the avoid ambiguous characters checkbox." @@ -628,7 +621,7 @@ "message": "Inne" }, "unlockMethods": { - "message": "Odblokuj Opcje" + "message": "Opcje odblokowania" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Ustaw metodę odblokowania, aby zmienić czas blokowania sejfu." @@ -985,7 +978,7 @@ "message": "Poproś o dodanie danych logowania" }, "vaultSaveOptionsTitle": { - "message": "Zapisz do ustawień sejfu" + "message": "Opcje zapisywania w sejfie" }, "addLoginNotificationDesc": { "message": "\"Dodaj powiadomienia logowania\" automatycznie wyświetla monit o zapisanie nowych danych logowania do sejfu przy każdym pierwszym logowaniu." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Pokaż elementy tożsamości na stronie głównej, aby ułatwić autouzupełnianie." }, + "clickToAutofillOnVault": { + "message": "Kliknij na dane logowania, aby autouzupełnić w widoku Sejfu" + }, "clearClipboard": { "message": "Wyczyść schowek", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Przenieś do organizacji" }, - "share": { - "message": "Udostępnij" - }, "movedItemToOrg": { "message": "Element $ITEMNAME$ został przeniesiony do organizacji $ORGNAME$", "placeholders": { @@ -1491,7 +1484,7 @@ "message": "Dotyczy wszystkich zalogowanych kont." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Wyłącz wbudowany w przeglądarkę menedżera haseł, aby uniknąć konfliktów." + "message": "Wyłącz wbudowany w przeglądarkę menedżer haseł, aby uniknąć konfliktów." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edytuj ustawienia przeglądarki." @@ -2047,9 +2040,6 @@ "clone": { "message": "Klonuj" }, - "passwordGeneratorPolicyInEffect": { - "message": "Co najmniej jedna zasada organizacji wpływa na ustawienia generatora." - }, "passwordGenerator": { "message": "Generator hasła" }, @@ -2334,6 +2324,9 @@ "message": "Domeny", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Wykluczone domeny" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Aplikacja Bitwarden nie będzie proponować zapisywania danych logowania dla tych domen dla wszystkich zalogowanych kont. Musisz odświeżyć stronę, aby zastosowywać zmiany." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Strona internetowa $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Zmiany w wykluczonych domenach zapisane" }, @@ -2389,14 +2394,6 @@ "message": "Szczegóły Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Szukaj w wysyłkach", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Dodaj wysyłkę", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Tekst" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Domyślnie ukryj tekst" }, - "maxAccessCountReached": { - "message": "Maksymalna liczba dostępów została osiągnięta", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Wygasła" }, - "pendingDeletion": { - "message": "Oczekiwanie na usunięcie" - }, "passwordProtected": { "message": "Chroniona hasłem" }, @@ -2472,24 +2462,9 @@ "message": "Edytuj wysyłkę", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Jakiego typu jest to wysyłka?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Nazwa wysyłki.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Plik, który chcesz wysłać." - }, "deletionDate": { "message": "Data usunięcia" }, - "deletionDateDesc": { - "message": "Wysyłka zostanie trwale usunięta w określonym czasie.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send zostanie trwale usunięte w tej dacie.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Data wygaśnięcia" }, - "expirationDateDesc": { - "message": "Jeśli funkcja jest włączona, dostęp do wysyłki wygaśnie po określonym czasie.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dzień" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Niestandardowe" }, - "maximumAccessCount": { - "message": "Maksymalna liczba dostępów" - }, - "maximumAccessCountDesc": { - "message": "Jeśli funkcja jest włączona, po osiągnięciu maksymalnej liczby dostępów, użytkownicy nie będą mieli dostępu do tej wysyłki.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Opcjonalne hasło dla użytkownika, aby uzyskać dostęp do wysyłki.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Zabezpiecz tę wiadomość hasłem, które będzie wymagane, aby uzyskać do niej dostęp.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Prywatne notatki o tej wysyłce.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Wyłącz wysyłkę, aby nikt nie miał do niej dostępu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Po zapisaniu wysyłki, skopiuj link do schowka.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Tekst, który chcesz wysłać." - }, - "sendHideText": { - "message": "Ukryj domyślnie tekst wysyłki.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Obecna liczba dostępów" - }, "createSend": { "message": "Nowa wysyłka", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Zanim zaczniesz" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Aby wybrać datę, ", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "kliknij tutaj", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "w celu otwarcia rozszerzenia w oknie.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Data wygaśnięcia nie jest prawidłowa." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Wystąpił błąd podczas zapisywania dat usunięcia i wygaśnięcia." }, - "hideEmail": { - "message": "Ukryj mój adres e-mail przed odbiorcami." - }, "hideYourEmail": { "message": "Ukryj mój adres e-mail przed oglądającymi." }, - "sendOptionsPolicyInEffect": { - "message": "Co najmniej jedna zasada organizacji wpływa na ustawienia wysyłek." - }, "passwordPrompt": { "message": "Potwierdź hasłem głównym" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Błąd" }, - "regenerateUsername": { - "message": "Wygeneruj ponownie nazwę użytkownika" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Wygeneruj nazwę użytkownika" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Rodzaj nazwy użytkownika" - }, "plusAddressedEmail": { "message": "Adres e-mail z plusem", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nazwa strony" }, - "whatWouldYouLikeToGenerate": { - "message": "Co chcesz wygenerować?" - }, - "passwordType": { - "message": "Rodzaj hasła" - }, "service": { "message": "Usługa" }, @@ -3503,38 +3425,6 @@ "message": "Zwiń/rozwiń", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Zaimportować Twoje dane do Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Ochronić Twoje dane LastPass i zaimportować do Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Zapisz jako niezaszyfrowany plik", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importuj do Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importowanie...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dane pomyślnie zaimportowane!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Błąd podczas importowania. Sprawdź konsolę, aby uzyskać szczegółowe informacje.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Wystąpił błąd sieci podczas importu.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Domena aliasu" }, @@ -4086,7 +3976,10 @@ "message": "Passkey został usunięty" }, "autofillSuggestions": { - "message": "Sugestie autouzupełnienia" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Zapisz element logowania dla tej witryny, aby automatycznie wypełnić" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden ma nowy wygląd!" - }, - "bitwardenNewLookDesc": { - "message": "Auto wypełnianie i szukanie na zakładce sejfu jest teraz prostsze i bardziej intuicyjne. Rozejrzyj się tam!" - }, "accountActions": { "message": "Akcje konta" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Nie masz uprawnień do edycji tego elementu" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Uwierzytelnianie" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index d7a8ae6ea4d..0971c5e1bc5 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Gerenciador de Senhas", + "message": "Gerenciador de senhas Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -193,10 +193,10 @@ "message": "Preenchimento automático identidade" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Preencher o código de verificação" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Preencher o código de verificação", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -242,7 +242,7 @@ "message": "Correio eletrônico da conta" }, "requestHint": { - "message": "Pedir dica" + "message": "Solicitar dica" }, "requestPasswordHint": { "message": "Dica da senha mestra" @@ -296,7 +296,7 @@ "message": "Continuar na extensão da loja do navegador?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Ajude outros a descobrir se o Bitwarden está certo para eles. Visite a loja de extensões do seu navegador e deixe uma classificação agora." + "message": "Ajude outras pessoas a descobrirem se o Bitwarden é o que elas estão procurando. Visite a loja de extensões do seu navegador e deixe uma classificação agora." }, "changeMasterPasswordOnWebConfirmation": { "message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden." @@ -340,7 +340,7 @@ "message": "Gerenciador de Segredos Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Armazene, gerencie e compartilhe segredos de desenvolvedor com o Gerenciador de segredos do Bitwarden. Saiba mais no site bitwarden.com." + "message": "Armazene, gerencie e compartilhe senhas de desenvolvedor com o Gerenciador de segredos do Bitwarden. Saiba mais no site bitwarden.com." }, "passwordlessDotDev": { "message": "Passwordless.dev" @@ -349,10 +349,10 @@ "message": "Crie experiências de login suaves e seguras, livres de senhas tradicionais com Passwordless.dev. Saiba mais no site bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Famílias do Bitwarden Grátis" + "message": "Plano Familiar do Bitwarden Grátis" }, "freeBitwardenFamiliesPageDesc": { - "message": "Você é elegível para as Famílias do Bitwarden Grátis. Resgate esta oferta hoje no aplicativo web." + "message": "Você é elegível para o plano Familiar do Bitwarden Grátis. Resgate esta oferta hoje no aplicativo web." }, "version": { "message": "Versão" @@ -454,9 +454,6 @@ "length": { "message": "Comprimento" }, - "passwordMinLength": { - "message": "Tamanho mínimo da senha" - }, "uppercase": { "message": "Maiúsculas (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,12 +525,8 @@ "minSpecial": { "message": "Especiais Mínimos" }, - "avoidAmbChar": { - "message": "Evitar Caracteres Ambíguos", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { - "message": "Evitar Caracteres Ambíguos", + "message": "Evitar caracteres ambíguos", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -898,7 +891,7 @@ "message": "Torne sua conta mais segura configurando o 'login' em duas etapas no aplicativo ‘web’ do Bitwarden." }, "twoStepLoginConfirmationTitle": { - "message": "Continuar para o aplicativo da ‘web’?" + "message": "Continuar para o aplicativo web?" }, "editedFolder": { "message": "Pasta Editada" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Liste os itens de identidade na aba atual para facilitar preenchimento automático." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Limpar Área de Transferência", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1130,7 +1126,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Atenção", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Mover para a Organização" }, - "share": { - "message": "Compartilhar" - }, "movedItemToOrg": { "message": "$ITEMNAME$ movido para $ORGNAME$", "placeholders": { @@ -1225,7 +1218,7 @@ "message": "Funcionalidade Indisponível" }, "encryptionKeyMigrationRequired": { - "message": "Migração de chave de criptografia necessária. Faça login através do cofre web para atualizar sua chave de criptografia." + "message": "É necessário migrar sua chave de criptografia. Faça login através do cofre web para atualizar sua chave de criptografia." }, "premiumMembership": { "message": "Assinatura Premium" @@ -1324,10 +1317,10 @@ "message": "Insira o código de verificação de 6 dígitos do seu aplicativo de autenticação." }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "Tempo de autenticação esgotado" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "A sessão de autenticação expirou. Por favor, reinicie o processo de login." }, "enterVerificationCodeEmail": { "message": "Insira o código de verificação de 6 dígitos que foi enviado por e-mail para $EMAIL$.", @@ -1491,7 +1484,7 @@ "message": "Aplica-se a todas as contas conectadas." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Desative as configurações do gerenciador de senhas do seu navegador para evitar conflitos." + "message": "Desative o gerenciador de senhas padrão do seu navegador para evitar conflitos." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Editar configurações do navegador." @@ -1853,7 +1846,7 @@ "message": "Notas seguras" }, "sshKeys": { - "message": "SSH Keys" + "message": "Chaves SSH" }, "clear": { "message": "Limpar", @@ -2047,9 +2040,6 @@ "clone": { "message": "Clonar" }, - "passwordGeneratorPolicyInEffect": { - "message": "Uma ou mais políticas da organização estão afetando as suas configurações do gerador." - }, "passwordGenerator": { "message": "Gerador de Senha" }, @@ -2199,7 +2189,7 @@ "message": "A sua nova senha mestra não cumpre aos requisitos da política." }, "receiveMarketingEmailsV2": { - "message": "Obtenha conselhos, novidades, e oportunidades de pesquisa do Bitwarden em sua caixa de entrada." + "message": "Obtenha dicas, novidades e oportunidades de pesquisa do Bitwarden em sua caixa de entrada." }, "unsubscribe": { "message": "Cancelar subscrição" @@ -2334,6 +2324,9 @@ "message": "Domínios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domínios Excluídos" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não irá pedir para salvar os detalhes de credencial para estes domínios. Você deve atualizar a página para que as alterações entrem em vigor." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Site $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Mudanças de domínios excluídos salvas" }, @@ -2389,14 +2394,6 @@ "message": "Enviar detalhes", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Pesquisar Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Adicionar Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Texto" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Ocultar texto por padrão" }, - "maxAccessCountReached": { - "message": "Número máximo de acessos atingido", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expirado" }, - "pendingDeletion": { - "message": "Exclusão pendente" - }, "passwordProtected": { "message": "Protegido por senha" }, @@ -2472,24 +2462,9 @@ "message": "Editar Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Que tipo de Send é este?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Um nome amigável para descrever este Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "O arquivo que você deseja enviar." - }, "deletionDate": { "message": "Data de Exclusão" }, - "deletionDateDesc": { - "message": "O Send será eliminado permanentemente na data e hora especificadas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "O envio será eliminado permanentemente na data e hora especificadas.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Data de Validade" }, - "expirationDateDesc": { - "message": "Se definido, o acesso a este Send expirará na data e hora especificadas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dia" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Personalizado" }, - "maximumAccessCount": { - "message": "Contagem Máxima de Acessos" - }, - "maximumAccessCountDesc": { - "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.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Exigir opcionalmente 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." - }, "sendPasswordDescV3": { "message": "Adicione uma senha opcional para os destinatários para acessar este Envio.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Notas privadas sobre esse Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Desative este Send para que ninguém possa acessá-lo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copiar o link deste Send para área de transferência após salvar.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "O texto que você deseja enviar." - }, - "sendHideText": { - "message": "Ocultar o texto deste Send por padrão.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Contagem Atual de Acessos" - }, "createSend": { "message": "Criar Novo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Antes de começar" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Para usar um seletor de data no estilo calendário", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "clique aqui", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "para abrir a sua janela.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "A data de validade fornecida não é válida." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Ocorreu um erro ao salvar as suas datas de exclusão e validade." }, - "hideEmail": { - "message": "Ocultar meu endereço de e-mail dos destinatários." - }, "hideYourEmail": { "message": "Ocultar meu endereço de correio eletrônico dos destinatários." }, - "sendOptionsPolicyInEffect": { - "message": "Uma ou mais políticas da organização estão afetando as suas opções de Send." - }, "passwordPrompt": { "message": "Solicitação nova de senha mestra" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Erro" }, - "regenerateUsername": { - "message": "Recriar Usuário" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Gerar Usuário" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Tipo de usuário" - }, "plusAddressedEmail": { "message": "E-mail alternativo (com um +)", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nome do Site" }, - "whatWouldYouLikeToGenerate": { - "message": "O que você gostaria de gerar?" - }, - "passwordType": { - "message": "Categoria de Senha" - }, "service": { "message": "Serviço" }, @@ -3175,25 +3097,25 @@ "message": "Reenviar notificação" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Visualizar todas as opções de login" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Visualizar todas as opções de login" }, "notificationSentDevice": { "message": "Uma notificação foi enviada para seu dispositivo." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Uma notificação foi enviada para o seu dispositivo" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Certifique-se que sua conta esteja desbloqueada e que a frase de identificação corresponda à do outro dispositivo" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Você será notificado assim que a requisição for aprovada" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Precisa de outra opção?" }, "loginInitiated": { "message": "Login iniciado" @@ -3289,16 +3211,16 @@ "message": "Abrir em uma nova janela" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Lembrar deste dispositivo para permanecer conectado" }, "deviceApprovalRequired": { "message": "Aprovação do dispositivo necessária. Selecione uma opção de aprovação abaixo:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Aprovação do dispositivo necessária" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Selecione uma opção de aprovação abaixo" }, "rememberThisDevice": { "message": "Lembrar deste dispositivo" @@ -3374,7 +3296,7 @@ "message": "E-mail do usuário ausente" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "E-mail de usuário ativo não encontrado. Desconectando." }, "deviceTrusted": { "message": "Dispositivo confiável" @@ -3503,38 +3425,6 @@ "message": "Alternar colapso", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar seus dados para o Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteja seus dados do LastPass e importe para o Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Salvar como arquivo não criptografado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar para o Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importando...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dados importados com sucesso!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro ao importar. Verifique o console para detalhes.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Erro de rede encontrado durante a importação.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias do domínio" }, @@ -3585,11 +3475,11 @@ "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "Código de Verificação TOTP", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "Tempo até expirar o código", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -3821,7 +3711,7 @@ "message": "Acessando" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Sessão Iniciada!" }, "passkeyNotCopied": { "message": "A chave de acesso não será copiada" @@ -4086,7 +3976,10 @@ "message": "Chave de acesso removida" }, "autofillSuggestions": { - "message": "Sugestões de autopreenchimento" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Salvar um item de login para este site autopreenchimento" @@ -4315,13 +4208,13 @@ "message": "Filtros" }, "filterVault": { - "message": "Filter vault" + "message": "Filtrar cofre" }, "filterApplied": { - "message": "One filter applied" + "message": "Um filtro aplicado" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "Foram aplicados $COUNT$ filtros", "placeholders": { "count": { "content": "$1", @@ -4664,12 +4557,6 @@ "textSends": { "message": "Texto enviado" }, - "bitwardenNewLook": { - "message": "Bitwarden tem uma nova aparência!" - }, - "bitwardenNewLookDesc": { - "message": "É mais fácil e mais intuitivo do que nunca autopreenchimento e pesquise na guia Cofre. Dê uma olhada ao redor!" - }, "accountActions": { "message": "Ações da conta" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Você não tem permissão para editar este arquivo" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autenticando" }, @@ -4911,22 +4825,22 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Aviso importante" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Configurar login em duas etapas" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden enviará um código para o seu e-mail para verificar novos dispositivos a partir de fevereiro de 2025." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Você pode configurar o login em duas etapas como uma forma alternativa de proteger sua conta ou mudar seu e-mail para um que você possa acessar." }, "remindMeLater": { - "message": "Remind me later" + "message": "Lembre-me depois" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Você tem acesso ao seu e-mail, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4935,24 +4849,24 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Não tenho" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Sim, posso acessar meu e-mail de forma confiável" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Ativar login em duas etapas" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Alterar e-mail" }, "extensionWidth": { - "message": "Extension width" + "message": "Largura da janela" }, "wide": { - "message": "Wide" + "message": "Grande" }, "extraWide": { - "message": "Extra wide" + "message": "Extra Grande" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 9bac24872e2..663e337d01c 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Comprimento" }, - "passwordMinLength": { - "message": "Comprimento mínimo da palavra-passe" - }, "uppercase": { "message": "Maiúsculas (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Mínimo de caracteres especiais" }, - "avoidAmbChar": { - "message": "Evitar caracteres ambíguos", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Evitar caracteres ambíguos", "description": "Label for the avoid ambiguous characters checkbox." @@ -568,7 +561,7 @@ "message": "Frase de acesso" }, "favorite": { - "message": "Favorito" + "message": "Adicionar aos favoritos" }, "unfavorite": { "message": "Remover dos favoritos" @@ -810,7 +803,7 @@ "message": "Código de verificação inválido" }, "valueCopied": { - "message": "$VALUE$ copiado", + "message": "$VALUE$ copiado(a)", "description": "Value has been copied to the clipboard.", "placeholders": { "value": { @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Listar itens de identidades na página Separador para facilitar o preenchimento automático." }, + "clickToAutofillOnVault": { + "message": "Clique nos itens para preencher automaticamente na vista do cofre" + }, "clearClipboard": { "message": "Limpar área de transferência", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Mover para a organização" }, - "share": { - "message": "Partilhar" - }, "movedItemToOrg": { "message": "$ITEMNAME$ movido para $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Duplicar" }, - "passwordGeneratorPolicyInEffect": { - "message": "Uma ou mais políticas da organização estão a afetar as suas definições do gerador." - }, "passwordGenerator": { "message": "Gerador de palavras-passe" }, @@ -2334,6 +2324,9 @@ "message": "Domínios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domínios bloqueados" + }, "excludedDomains": { "message": "Domínios excluídos" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não pedirá para guardar os detalhes de início de sessão destes domínios para todas as contas com sessão iniciada. É necessário atualizar a página para que as alterações tenham efeito." }, + "blockedDomainsDesc": { + "message": "O preenchimento automático e outras funcionalidades relacionadas não serão disponibilizados para estes sites. É necessário atualizar a página para que as alterações tenham efeito." + }, + "autofillBlockedNoticeV2": { + "message": "O preenchimento automático está bloqueado para este site." + }, + "autofillBlockedNoticeGuidance": { + "message": "Alterar esta opção nas definições" + }, "websiteItemLabel": { "message": "Site $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Alterações do domínio bloqueado guardadas" + }, "excludedDomainsSavedSuccess": { "message": "Alterações do domínio excluído guardadas" }, @@ -2389,14 +2394,6 @@ "message": "Detalhes do Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Procurar Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Adicionar Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Texto" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Ocultar texto por predefinição" }, - "maxAccessCountReached": { - "message": "Número máximo de acessos atingido", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expirado" }, - "pendingDeletion": { - "message": "Eliminação pendente" - }, "passwordProtected": { "message": "Protegido por palavra-passe" }, @@ -2472,24 +2462,9 @@ "message": "Editar Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Que tipo de Send é este?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Um nome simpático para descrever este Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "O ficheiro que deseja enviar." - }, "deletionDate": { "message": "Data de eliminação" }, - "deletionDateDesc": { - "message": "O Send será permanentemente eliminado na data e hora especificadas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "O Send será permanentemente eliminado nesta data.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Prazo de validade" }, - "expirationDateDesc": { - "message": "Se definido, o acesso a este Send expirará na data e hora especificadas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dia" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Personalizado" }, - "maximumAccessCount": { - "message": "Número máximo de acessos" - }, - "maximumAccessCountDesc": { - "message": "Se definido, os utilizadores deixarão de poder aceder a este Send quando a contagem máxima de acessos for atingida.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Opcionalmente, exigir uma palavra-passe para os utilizadores acederem a este Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Adicione uma palavra-passe opcional para os destinatários acederem a 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.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Desative este Send para que ninguém possa aceder ao mesmo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copiar o link deste Send para a área de transferência ao guardar.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "O texto que deseja enviar." - }, - "sendHideText": { - "message": "Ocultar o texto deste Send por defeito.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Número de acessos atual" - }, "createSend": { "message": "Novo Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Antes de começar" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Para utilizar um seletor de datas do tipo calendário,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "clique aqui", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "para abrir a janela.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "O prazo de validade fornecido não é válido." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Ocorreu um erro ao guardar as suas datas de eliminação e validade." }, - "hideEmail": { - "message": "Ocultar o meu endereço de e-mail dos destinatários." - }, "hideYourEmail": { "message": "Oculte o seu endereço de e-mail dos visualizadores." }, - "sendOptionsPolicyInEffect": { - "message": "Uma ou mais políticas da organização estão a afetar as suas opções do Send." - }, "passwordPrompt": { "message": "Pedir novamente a palavra-passe mestra" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Erro" }, - "regenerateUsername": { - "message": "Regenerar nome de utilizador" + "decryptionError": { + "message": "Erro de desencriptação" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "O Bitwarden não conseguiu desencriptar o(s) item(ns) do cofre listado(s) abaixo." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacte o serviço de apoio ao cliente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "para evitar perdas adicionais de dados.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Gerar nome de utilizador" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Tipo de nome de utilizador" - }, "plusAddressedEmail": { "message": "E-mail com subendereço", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nome do site" }, - "whatWouldYouLikeToGenerate": { - "message": "O que é que gostaria de gerar?" - }, - "passwordType": { - "message": "Tipo de palavra-passe" - }, "service": { "message": "Serviço" }, @@ -3503,38 +3425,6 @@ "message": "Alternar colapso", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importar os seus dados para o Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Proteger os seus dados LastPass e importar para o Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Guardar como ficheiro não encriptado", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importar para o Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "A importar...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dados importados com sucesso!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Erro de importação. Verifique a consola para obter detalhes.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Erro de rede encontrado durante a importação.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias de domínio" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Sugestões de preenchimento automático" }, + "itemSuggestions": { + "message": "Itens sugeridos" + }, "autofillSuggestionsTip": { "message": "Guarde uma credencial deste site para preenchimento automático" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Sends de texto" }, - "bitwardenNewLook": { - "message": "O Bitwarden tem um novo visual!" - }, - "bitwardenNewLookDesc": { - "message": "É mais fácil e mais intuitivo do que nunca preencher automaticamente e pesquisar a partir do separador Cofre. Dê uma vista de olhos!" - }, "accountActions": { "message": "Ações da conta" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Não tem permissão para editar este item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "O desbloqueio biométrico não está disponível porque o desbloqueio por PIN ou palavra-passe é necessário primeiro." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "O desbloqueio biométrico está atualmente indisponível." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "O desbloqueio biométrico não está disponível porque a app para computador Bitwarden está fechada." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "O desbloqueio biométrico não está disponível porque não está ativado para $EMAIL$ na app Bitwarden para computador.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "O desbloqueio biométrico está atualmente indisponível por um motivo desconhecido." + }, "authenticating": { "message": "A autenticar" }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index cde1f233488..165c3749b53 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Lungime" }, - "passwordMinLength": { - "message": "Lungimea minimă a parolei" - }, "uppercase": { "message": "Litere mari (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minim de caractere speciale" }, - "avoidAmbChar": { - "message": "Se evită caracterele ambigue", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Listați elementelor de identitate de pe pagina Filă pentru a facilita completarea automată." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Golire clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Mutare la organizație" }, - "share": { - "message": "Partajare" - }, "movedItemToOrg": { "message": "$ITEMNAME$ mutat la $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clonare" }, - "passwordGeneratorPolicyInEffect": { - "message": "Una sau mai multe politici organizaționale vă afectează setările generatorului." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domenii excluse" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Căutare Send-uri", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Adăugare Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "S-a atins numărul maxim de accesări", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expirat" }, - "pendingDeletion": { - "message": "Ștergere în așteptare" - }, "passwordProtected": { "message": "Protejat cu parolă" }, @@ -2472,24 +2462,9 @@ "message": "Editare Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Ce fel de Send este acesta?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Un nume prietenos pentru a descrie acest Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Fișierul pe care doriți să-l trimiteți." - }, "deletionDate": { "message": "Data ștergerii" }, - "deletionDateDesc": { - "message": "Send-ul va fi șters definitiv la data și ora specificate.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Data expirării" }, - "expirationDateDesc": { - "message": "Dacă este setat, accesul la acest Send va expira la data și ora specificate.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 zi" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Personalizat" }, - "maximumAccessCount": { - "message": "Număr maxim de accesări" - }, - "maximumAccessCountDesc": { - "message": "Dacă este configurat, utilizatorii nu vor mai putea accesa acest Send când a fost atins numărul maxim de accesări.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Opțional, este necesară o parolă pentru ca utilizatorii să acceseze acest Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Note private despre acest Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Dezactivați acest Send pentru ca nimeni să nu-l poată accesa.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copiază acest link de Send în clipboard la salvare.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Textul pe care doriți să-l trimiteți." - }, - "sendHideText": { - "message": "Ascunde în mod implicit textul acestui Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Numărul actual de accesări" - }, "createSend": { "message": "Nou Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Înainte de a începe" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Pentru a utiliza un selector de date în stil calendar,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "faceți clic aici", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "pentru ca fereastra dvs să apară.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Data de expirare furnizată nu este validă." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "A survenit o eroare la salvarea datelor de ștergere și de expirare." }, - "hideEmail": { - "message": "Ascundeți adresa mea de e-mail de la destinatari." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Una sau mai multe politici organizaționale vă afectează opțiunile Send-ului." - }, "passwordPrompt": { "message": "Re-solicitare parolă principală" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Eroare" }, - "regenerateUsername": { - "message": "Regenerare nume de utilizator" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generare nume de utilizator" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Tip de nume de utilizator" - }, "plusAddressedEmail": { "message": "E-mail Plus adresat", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Nume website" }, - "whatWouldYouLikeToGenerate": { - "message": "Ce doriți să generați?" - }, - "passwordType": { - "message": "Tip de parolă" - }, "service": { "message": "Serviciu" }, @@ -3503,38 +3425,6 @@ "message": "Comutare restrângere", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index a0279460363..5519737f16c 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Длина" }, - "passwordMinLength": { - "message": "Минимальная длина пароля" - }, "uppercase": { "message": "Прописные буквы (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Минимум символов" }, - "avoidAmbChar": { - "message": "Избегать неоднозначных символов", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Избегать неоднозначных символов", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Личности будут отображены на вкладке для удобного автозаполнения." }, + "clickToAutofillOnVault": { + "message": "Кликните элементы для автозаполнения в режиме просмотра хранилища" + }, "clearClipboard": { "message": "Очистить буфер обмена", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Переместить в организацию" }, - "share": { - "message": "Поделиться" - }, "movedItemToOrg": { "message": "$ITEMNAME$ перемещен в $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Клонировать" }, - "passwordGeneratorPolicyInEffect": { - "message": "На настройки генератора влияют одна или несколько политик организации." - }, "passwordGenerator": { "message": "Генератор паролей" }, @@ -2334,6 +2324,9 @@ "message": "Домены", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Заблокированные домены" + }, "excludedDomains": { "message": "Исключенные домены" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не будет предлагать сохранение логинов для этих доменов для всех авторизованных аккаунтов. Для вступления изменений в силу необходимо обновить страницу." }, + "blockedDomainsDesc": { + "message": "Автозаполнение и другие связанные с ним функции не будут предлагаться для этих сайтов. Чтобы изменения вступили в силу, необходимо обновить страницу." + }, + "autofillBlockedNoticeV2": { + "message": "Автозаполнение для этого сайта заблокировано." + }, + "autofillBlockedNoticeGuidance": { + "message": "Измените это в настройках" + }, "websiteItemLabel": { "message": "Сайт $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Изменения в заблокированном домене сохранены" + }, "excludedDomainsSavedSuccess": { "message": "Изменения в исключенном домене сохранены" }, @@ -2389,14 +2394,6 @@ "message": "Информация о Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Поиск Send’ов", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Добавить Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Текст" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Скрыть текст по умолчанию" }, - "maxAccessCountReached": { - "message": "Достигнут максимум обращений", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Срок истек" }, - "pendingDeletion": { - "message": "Ожидание удаления" - }, "passwordProtected": { "message": "Защищено паролем" }, @@ -2472,24 +2462,9 @@ "message": "Изменить Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Выберите тип Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Понятное имя для описания этой Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Файл, который вы хотите отправить." - }, "deletionDate": { "message": "Дата удаления" }, - "deletionDateDesc": { - "message": "Эта Send будет окончательно удалена в указанные дату и время.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "С этой даты Send будет удалена навсегда.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Дата истечения" }, - "expirationDateDesc": { - "message": "Если задано, доступ к этой Send истечет в указанные дату и время.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 день" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Пользовательский" }, - "maximumAccessCount": { - "message": "Максимум обращений" - }, - "maximumAccessCountDesc": { - "message": "Если задано, пользователи больше не смогут получить доступ к этой Send, как только будет достигнуто максимальное количество обращений.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "По возможности запрашивать у пользователей пароль для доступа к этой Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Добавьте опциональный пароль для доступа получателей к этой Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Личные заметки об этой Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Деактивировать эту Send, чтобы никто не мог получить к ней доступ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Скопировать ссылку на эту Send в буфер обмена после сохранения.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Текст, который вы хотите отправить." - }, - "sendHideText": { - "message": "Скрыть текст этой Send по умолчанию.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Текущих обращений" - }, "createSend": { "message": "Новая Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Перед тем, как начать" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Для использования календарного стиля выбора даты,", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "нажмите здесь", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "для открытия в новом окне.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Срок истечения указан некорректно." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Произошла ошибка при сохранении данных о сроках удаления и истечения." }, - "hideEmail": { - "message": "Скрыть мой адрес email от получателей." - }, "hideYourEmail": { "message": "Скрыть ваш email от просматривающих." }, - "sendOptionsPolicyInEffect": { - "message": "На параметры Send влияют одна или несколько политик организации." - }, "passwordPrompt": { "message": "Повторный запрос мастер-пароля" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Ошибка" }, - "regenerateUsername": { - "message": "Пересоздать имя пользователя" + "decryptionError": { + "message": "Ошибка расшифровки" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden не удалось расшифровать элемент(ы) хранилища, перечисленные ниже." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Обратитесь в службу поддержки,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "чтобы избежать дополнительной потери данных.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Создать имя пользователя" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Тип имени пользователя" - }, "plusAddressedEmail": { "message": "Субадресованные email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Название сайта" }, - "whatWouldYouLikeToGenerate": { - "message": "Что вы хотите сгенерировать?" - }, - "passwordType": { - "message": "Тип пароля" - }, "service": { "message": "Служба" }, @@ -3503,38 +3425,6 @@ "message": "Свернуть/развернуть", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Импортировать данные в Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Защитить данные LastPass и импортировать их в Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Сохранить как незашифрованный файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Импортировать в Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Импорт...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Данные успешно импортированы!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Ошибка импорта. Проверьте консоль для получения подробной информации.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Во время импорта возникла сетевая ошибка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдоним домена" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Предложения по автозаполнению" }, + "itemSuggestions": { + "message": "Предлагаемые элементы" + }, "autofillSuggestionsTip": { "message": "Сохранить логин для этого сайта для автозаполнения" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Текстовая Send" }, - "bitwardenNewLook": { - "message": "У Bitwarden новый облик!" - }, - "bitwardenNewLookDesc": { - "message": "Теперь автозаполнение и поиск на вкладке Хранилище стали проще и интуитивно понятнее, чем когда-либо. Осмотритесь!" - }, "accountActions": { "message": "Действия аккаунта" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "У вас нет разрешения на редактирование этого элемента" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Биометрическая разблокировка недоступна, поскольку сначала требуется разблокировка с помощью PIN-кода или пароля." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Биометрическая разблокировка в настоящее время недоступна." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Биометрическая разблокировка недоступна, поскольку Bitwarden для компьютера закрыт." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Биометрическая разблокировка недоступна, потому что она не включена для $EMAIL$ в приложении Bitwarden для компьютера.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Биометрическая разблокировка в настоящее время недоступна по неизвестной причине." + }, "authenticating": { "message": "Аутентификация" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 624e6f6f56e..801e088aca8 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -454,9 +454,6 @@ "length": { "message": "දිග" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "අවම විශේෂ" }, - "avoidAmbChar": { - "message": "අපැහැදිලි චරිත වලින් වළකින්න", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "පසුරු පුවරුවට පැහැදිලි", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "සංවිධානය වෙත ගෙනයන්න" }, - "share": { - "message": "බෙදාගන්න" - }, "movedItemToOrg": { "message": "$ITEMNAME$ $ORGNAME$වෙත ගෙන ගියේය", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "ක්ලෝන" }, - "passwordGeneratorPolicyInEffect": { - "message": "සංවිධාන ප්රතිපත්ති එකක් හෝ වැඩි ගණනක් ඔබේ උත්පාදක සැකසුම් වලට බලපායි." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "බැහැර වසම්" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "සෙවුම් යවයි", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "යවන්න එකතු කරන්න", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "පෙළ" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "මැක්ස් ප්රවේශ ගණන ළඟා", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "කල් ඉකුත්" }, - "pendingDeletion": { - "message": "මකාදැමීම" - }, "passwordProtected": { "message": "මුරපදය ආරක්ෂා" }, @@ -2472,24 +2462,9 @@ "message": "යැවීම සංස්කරණය", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "මෙය කුමන ආකාරයේ යවන්න ද?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "මෙම යවන්න විස්තර කිරීමට මිත්රශීලී නමක්.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "ඔබට යැවීමට අවශ්ය ගොනුව." - }, "deletionDate": { "message": "මකාදැමීමේ දිනය" }, - "deletionDateDesc": { - "message": "නියම කරන ලද දිනය හා වේලාව මත Send ස්ථිරවම මකා දමනු ලැබේ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "කල් ඉකුත්වන දිනය" }, - "expirationDateDesc": { - "message": "සකසා ඇත්නම්, මෙම යවන්න වෙත ප්රවේශය නිශ්චිත දිනය හා වේලාව කල් ඉකුත් වනු ඇත.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "දින 1" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "අභිරුචි" }, - "maximumAccessCount": { - "message": "උපරිම ප්රවේශ ගණන්" - }, - "maximumAccessCountDesc": { - "message": "සකසා ඇත්නම්, උපරිම ප්රවේශ ගණන ළඟා වූ පසු පරිශීලකයින්ට මෙම Send වෙත ප්රවේශ වීමට තවදුරටත් නොහැකි වනු ඇත.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "විකල්පයක් ලෙස පරිශීලකයින්ට මෙම යවන්න වෙත ප්රවේශ වීමට මුරපදයක් අවශ්ය වේ.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "මේ ගැන පෞද්ගලික සටහන් යවන්න.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "මෙය අක්රීය කරන්න යවන්න එවිට කිසිවෙකුට එයට ප්රවේශ විය නොහැක.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "සුරකින්න මත මෙම යවන්න ගේ සබැඳිය පසුරු පුවරුවට පිටපත් කරන්න.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "ඔබට යැවීමට අවශ්ය පෙළ." - }, - "sendHideText": { - "message": "මෙම යවන්න පෙළ පෙරනිමියෙන් සඟවන්න.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "වත්මන් ප්රවේශ ගණන්" - }, "createSend": { "message": "නව යවන්න නිර්මාණය", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "ඔබ ආරම්භ කිරීමට පෙර" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "දින දර්ශනය ශෛලිය දිනය ජීව අත්බෝම්බයක් සමග භාවිතා කිරීමට", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "මෙහි ක්ලික් කරන්න", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "ඔබේ කවුළුව දිස්වේ.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "ලබා දී ඇති කල් ඉකුත්වන දිනය වලංගු නොවේ." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "ඔබගේ මකාදැමීම සහ කල් ඉකුත් වීමේ දිනයන් ඉතිරි කිරීමේ දෝෂයක් තිබුණි." }, - "hideEmail": { - "message": "ලබන්නන්ගෙන් මගේ විද්යුත් තැපැල් ලිපිනය සඟවන්න." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "සංවිධාන ප්රතිපත්ති එකක් හෝ කිහිපයක් ඔබගේ Send විකල්පයන්ට බලපායි." - }, "passwordPrompt": { "message": "ප්රධාන මුරපදය නැවත විමසුමක්" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 48c5007b54b..92d0e35a51c 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Dĺžka" }, - "passwordMinLength": { - "message": "Minimálna dĺžka hesla" - }, "uppercase": { "message": "Veľké písmená (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum špeciálnych znakov" }, - "avoidAmbChar": { - "message": "Vyhnúť sa zameniteľným znakom", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Vyhnúť sa zameniteľným znakom", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Zoznam položiek identity na stránke \"Aktuálna karta\" na jednoduché automatické vypĺňanie." }, + "clickToAutofillOnVault": { + "message": "Kliknutím na položku v trezore automaticky vyplniť" + }, "clearClipboard": { "message": "Vymazať schránku", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Presunúť do organizácie" }, - "share": { - "message": "Zdieľať" - }, "movedItemToOrg": { "message": "$ITEMNAME$ presunuté do $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Klonovať" }, - "passwordGeneratorPolicyInEffect": { - "message": "Jedno alebo viac nastavení organizácie ovplyvňujú vaše nastavenia generátora." - }, "passwordGenerator": { "message": "Generátor hesla" }, @@ -2334,6 +2324,9 @@ "message": "Domény", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokované domény" + }, "excludedDomains": { "message": "Vylúčené domény" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude požadovať ukladanie prihlasovacích údajov pre tieto domény pre všetky prihlásené účty. Aby sa zmeny prejavili, musíte stránku obnoviť." }, + "blockedDomainsDesc": { + "message": "Automatické vypĺňanie a ďalšie súvisiace funkcie sa na týchto webových stránkach nebudú ponúkať. Aby sa zmeny prejavili, musíte stránku obnoviť." + }, + "autofillBlockedNoticeV2": { + "message": "Automatické vypĺňanie je pre túto webovú stránku zablokované." + }, + "autofillBlockedNoticeGuidance": { + "message": "Zmeňte to v nastaveniach" + }, "websiteItemLabel": { "message": "Webstránka $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Zmeny v blokovaných doménach boli uložené" + }, "excludedDomainsSavedSuccess": { "message": "Uložené zmeny vylúčenej domény" }, @@ -2389,14 +2394,6 @@ "message": "Podrobnosti o Sende", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Hľadať Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Pridať Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "V predvolenom nastavení skryť text" }, - "maxAccessCountReached": { - "message": "Bol dosiahnutý maximálny počet prístupov", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expirované" }, - "pendingDeletion": { - "message": "Čakajúce odstránenie" - }, "passwordProtected": { "message": "Chránené heslom" }, @@ -2472,24 +2462,9 @@ "message": "Upraviť Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Aký typ Sendu to je?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Priateľský názov pre popísanie tohto Sendu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Súbor, ktorý chcete odoslať." - }, "deletionDate": { "message": "Dátum odstránenia" }, - "deletionDateDesc": { - "message": "Send bude natrvalo odstránený v zadaný dátum a čas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send bude natrvalo odstránený v tento deň.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Dátum exspirácie" }, - "expirationDateDesc": { - "message": "Ak je nastavené, prístup k tomuto Sendu vyprší v zadaný dátum a čas.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 deň" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Vlastné" }, - "maximumAccessCount": { - "message": "Maximálny počet prístupov" - }, - "maximumAccessCountDesc": { - "message": "Ak je nastavené, používatelia už nebudú mať prístup k tomuto Sendu po dosiahnutí maximálneho počtu prístupov.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Voliteľne môžete vyžadovať heslo pre používateľov na prístup k tomuto Sendu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Pridajte voliteľné heslo pre príjemcov na prístup k tomuto Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Zabezpečená poznámka o tomto Sende.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Vypnúť tento Send, aby k nemu nikto nemal prístup.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Pri uložení kopírovať odkaz na Send do schránky.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Text, ktorý chcete odoslať." - }, - "sendHideText": { - "message": "Predvolene skryť text tohto Sendu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Súčasný počet prístupov" - }, "createSend": { "message": "Vytvoriť nový Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Skôr než začnete" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Ak chcete použiť pre výber dátumu štýl kalendára", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "kliknite sem", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "a vysunie sa.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Uvedený dátum exspirácie nie je platný." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Pri ukladaní dátumov odstránenia a vypršania platnosti sa vyskytla chyba." }, - "hideEmail": { - "message": "Skryť moju emailovú adresu pred príjemcami." - }, "hideYourEmail": { "message": "Skryť moju e-mailovú adresu pri zobrazení." }, - "sendOptionsPolicyInEffect": { - "message": "Jedno alebo viac pravidiel organizácie ovplyvňujú vaše možnosti funkcie Send." - }, "passwordPrompt": { "message": "Znova zadajte hlavné heslo" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Chyba" }, - "regenerateUsername": { - "message": "Vygenerovať nové používateľské meno" + "decryptionError": { + "message": "Chyba dešifrovania" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nedokázal dešifrovať nižšie uvedené položky trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznícku podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "aby ste predišli ďalším stratám údajov.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Vygenerovať používateľské meno" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Typ používateľského mena" - }, "plusAddressedEmail": { "message": "E-mail s plusovým aliasom", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Názov stránky" }, - "whatWouldYouLikeToGenerate": { - "message": "Čo by ste chceli vygenerovať?" - }, - "passwordType": { - "message": "Typ hesla" - }, "service": { "message": "Služba" }, @@ -3503,38 +3425,6 @@ "message": "Prepnúť zbalenie", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importovať údaje do Bitwardenu?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Ochrániť údaje z LastPassu a importovať ich do Bitwardenu?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Uložiť ako nezašifrovaný súbor", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importovať do Bitwardenu", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importovanie...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Import údajov prebehol úspešne!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Chyba pri importovaní. Podrobnosti nájdete v konzole.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Počas importu sa vyskytla chyba siete.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias doména" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Návrhy automatického vypĺňania" }, + "itemSuggestions": { + "message": "Navrhované položky" + }, "autofillSuggestionsTip": { "message": "Uložte položku prihlásenia pre tento web na automatické vyplnenie" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Textové Sendy" }, - "bitwardenNewLook": { - "message": "Bitwarden má nový vzhľad!" - }, - "bitwardenNewLookDesc": { - "message": "Automatické vypĺňanie a vyhľadávanie na karte Trezor je jednoduchšie a intuitívnejšie ako kedykoľvek predtým. Poobzerajte sa!" - }, "accountActions": { "message": "Operácie s účtom" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Na úpravu tejto položky nemáte oprávnenie" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné pretože je najskôr potrebné odomykanie pomocou PIN alebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Odomykanie biometrickými údajmi je momentálne nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Odomykanie biometrickými údajmi je nedostupné, pretože aplikácia Bitwarden pre desktop je zatvorená." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Odomykanie biometrickými údajmi je nedostupné, pretože nie je povolené pre $EMAIL$ v aplikácii Bitwarden pre desktop.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Odomykanie biometrickými údajmi je momentálne z neznámych dôvodov nedostupné." + }, "authenticating": { "message": "Overuje sa" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 823b2ae0f18..d57d6ec5e57 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Dolžina" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Velike črke (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimalno posebnih znakov" }, - "avoidAmbChar": { - "message": "Izogibaj se dvoumnim znakom", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Na strani Zavihek prikaži elemente identitete za lažje samodejno izpolnjevanje." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Počisti odložišče", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Premakni v organizacijo" }, - "share": { - "message": "Deli" - }, "movedItemToOrg": { "message": "Element $ITEMNAME$ premaknjen v $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Podvoji" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Izključene domene" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Išči pošiljke", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Dodaj pošiljko", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Besedilo" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Poteklo" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Uredi pošiljko", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Kakšna vrsta pošiljke je to?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Prijazno ime, ki opisuje to pošiljko", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Datoteka, ki jo želite poslati" - }, "deletionDate": { "message": "Datum izbrisa" }, - "deletionDateDesc": { - "message": "Pošiljka bo trajno izbrisana ob izbranem času.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Datum poteka" }, - "expirationDateDesc": { - "message": "Če to nastavite, bo pošiljka potekla ob izbranem času.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dan" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Po meri" }, - "maximumAccessCount": { - "message": "Največje dovoljeno število dostopov" - }, - "maximumAccessCountDesc": { - "message": "Če to nastavite, uporabniki po določenem številu dostopov ne bodo mogli več dostopati do pošiljke.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Za dostop do te pošiljke lahko nastavite geslo.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Zasebni zapiski o tej pošiljki.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Onemogoči to pošiljko, da nihče ne more dostopati do nje.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopiraj povezavo te pošiljke v odložišče, ko shranim.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Besedilo, ki ga želite poslati" - }, - "sendHideText": { - "message": "Privzeto skrij besedilo te pošiljke.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Trenutno število dstopov" - }, "createSend": { "message": "Nova pošiljka", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Preden pričnete" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Za vnos datuma s pomočjo koledarčka", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "kliknite tukaj", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "za prikaz v lastnem oknu.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Datum poteka ni veljaven." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Pri shranjevanju datumov poteka in izbrisa je prišlo do napake." }, - "hideEmail": { - "message": "Skrij moj e-naslov pred prejemniki." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Nekatere nastavitve organizacije vplivajo na možnosti, ki jih imate v zvezi s pošiljkami." - }, "passwordPrompt": { "message": "Ponovno zahtevaj glavno geslo" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Napaka" }, - "regenerateUsername": { - "message": "Ponovno ustvari uporabniško ime" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Ustvari uporabniško ime" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Vrsta uporabniškega imena" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Ime spletne strani" }, - "whatWouldYouLikeToGenerate": { - "message": "Kaj želite generirati?" - }, - "passwordType": { - "message": "Vrsta gesla" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index e940c908d6f..80de0ba3c8e 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -193,10 +193,10 @@ "message": "Ауто-пуњење идентитета" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Пуни верификациони кôд" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Пуни верификациони кôд", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -454,9 +454,6 @@ "length": { "message": "Дужина" }, - "passwordMinLength": { - "message": "Минимална дужина лозинке" - }, "uppercase": { "message": "Велика слова (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Минимално специјалних знакова" }, - "avoidAmbChar": { - "message": "Избегавај двосмислене карактере", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Избегавај двосмислене карактере", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Прикажи ставке идентитета на страници за лакше аутоматско допуњавање." }, + "clickToAutofillOnVault": { + "message": "Кликните на ставке за ауто-попуњавање у приказу сефа" + }, "clearClipboard": { "message": "Обриши привремену меморију", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Премести у организацију" }, - "share": { - "message": "Подели" - }, "movedItemToOrg": { "message": "$ITEMNAME$ премештен у $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Клонирај" }, - "passwordGeneratorPolicyInEffect": { - "message": "Једна или више смерница организације утичу на поставке вашег генератора." - }, "passwordGenerator": { "message": "Генератор Лозинке" }, @@ -2334,6 +2324,9 @@ "message": "Домени", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Изузети домени" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Сајт $number$ (УРЛ)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Изузете промене домена су сачуване" }, @@ -2389,14 +2394,6 @@ "message": "Детаљи Send-а", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Тражи „Send“", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Додај „Send“", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Текст" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Сакриј текст подразумевано" }, - "maxAccessCountReached": { - "message": "Достигнут максималан број приступа", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Истекло" }, - "pendingDeletion": { - "message": "Брисање на чекању" - }, "passwordProtected": { "message": "Заштићено лозинком" }, @@ -2472,24 +2462,9 @@ "message": "Уреди „Send“", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Који је ово тип „Send“-a?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Име да се опише овај „Send“.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Датотека коју желиш да пошаљеш." - }, "deletionDate": { "message": "Брисање после" }, - "deletionDateDesc": { - "message": "„Send“ ће бити трајно избрисан наведеног датума и времена.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Send ће бити трајно обрисано у наведени датум.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Рок употребе" }, - "expirationDateDesc": { - "message": "Ако је постављено, приступ овом „Send“ истиче на наведени датум и време.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 дан" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Друго" }, - "maximumAccessCount": { - "message": "Максималан број приступа" - }, - "maximumAccessCountDesc": { - "message": "Ако је постављено, корисници више неће моћи да приступе овом „Send“ када се достигне максимални број приступа.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Опционално захтевајте лозинку за приступ корисницима „Send“-у.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Додајте опционалну лозинку за примаоце да приступе овом Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Приватне белешке о овом „Send“.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Онемогућите овај „Send“ да нико не би могао да му приступи.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Након чувања, копирај УРЛ за овај „Send“ у привремену меморију.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Текст који желиш да пошаљеш." - }, - "sendHideText": { - "message": "Подразумевано сакриј текст за овај „Send“.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Тренутни број приступа" - }, "createSend": { "message": "Креирај нови „Send“", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Пре него што почнеш" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Да би користио бирање датума кроз календар", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "кликните овде", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "Да бисте приказали искачући прозор.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Наведени датум истека није исправан." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Појавила се грешка при чувању датума брисања и истека." }, - "hideEmail": { - "message": "Сакриј моју е-адресу од примаоца." - }, "hideYourEmail": { "message": "Сакријте свој имејл од гледалаца." }, - "sendOptionsPolicyInEffect": { - "message": "Једна или више смерница организације утичу на опције „Send“-а." - }, "passwordPrompt": { "message": "Поновно тражење главне лозинке" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Грешка" }, - "regenerateUsername": { - "message": "Поново генериши име" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Генериши име" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Тип имена" - }, "plusAddressedEmail": { "message": "Плус имејл адресе", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Име Вашег веб-сајта" }, - "whatWouldYouLikeToGenerate": { - "message": "Шта желите да генеришете?" - }, - "passwordType": { - "message": "Тип лозинке" - }, "service": { "message": "Сервис" }, @@ -3178,7 +3100,7 @@ "message": "Погледајте сав извештај у опције" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Погледајте сав извештај у опције" }, "notificationSentDevice": { "message": "Обавештење је послато на ваш уређај." @@ -3503,38 +3425,6 @@ "message": "Промени проширење", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Увезите своје податке у Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Заштитите своје LastPass податке и увезите у Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Сачувати као нешифровану датотеку", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Увоз у Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Увоз...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Подаци су успешно увезени!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Грешка при увозу. Проверите конзолу за детаље.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Дошло је до грешке на мрежи током увоза.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Домен алијаса" }, @@ -3585,11 +3475,11 @@ "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "Једнократни верификациони кôд заснован на времену", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "Преостало време до истека актуелног ТОТП-а", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -4086,7 +3976,10 @@ "message": "Приступни кључ је уклоњен" }, "autofillSuggestions": { - "message": "Предлози за ауто-попуњавање" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Сачувајте ставку за пријаву за ову локацију за ауто-попуњавање" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Текст „Send“" }, - "bitwardenNewLook": { - "message": "Bitwarden има нови изглед!" - }, - "bitwardenNewLookDesc": { - "message": "Лакше је и интуитивније него икада да се аутоматски попуњава и тражи са картице Сефа. Проверите!" - }, "accountActions": { "message": "Акције везане за налог" }, @@ -4677,7 +4564,7 @@ "message": "Прикажи број предлога за ауто-попуњавање пријаве на икони додатка" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "Приказати брзе радње копирања у Сефу" }, "systemDefault": { "message": "Системски подразумевано" @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Немате дозволу да уређујете ову ставку" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Аутентификација" }, @@ -4911,22 +4825,22 @@ "message": "Бета" }, "importantNotice": { - "message": "Important notice" + "message": "Важно обавештење" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Поставити дво-степенску пријаву" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "Bitwarden ће послати кôд на имејл вашег налога за верификовање пријављивања са нових уређаја почевши од фебруара 2025." }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "Можете да подесите пријаву у два корака као алтернативни начин да заштитите свој налог или да промените свој имејл у један који можете да приступите." }, "remindMeLater": { - "message": "Remind me later" + "message": "Подсети ме касније" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "Да ли имате поуздан приступ својим имејлом, $EMAIL$?", "placeholders": { "email": { "content": "$1", @@ -4935,16 +4849,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Не, ненам" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Да, могу поуздано да приступим овим имејлом" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "Упалити дво-степенску пријаву" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Променити имејл налога" }, "extensionWidth": { "message": "Ширина додатка" diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 3cfb2feb37d..1927f12bc27 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Längd" }, - "passwordMinLength": { - "message": "Minsta tillåtna lösenordslängd" - }, "uppercase": { "message": "Versaler (A-Ö)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minsta antal speciella tecken" }, - "avoidAmbChar": { - "message": "Undvik tvetydiga tecken", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Undvik tvetydiga tecken", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Lista identitetsobjekt på fliksidan för enkel automatisk fyllning." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Rensa urklipp", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Flytta till organisation" }, - "share": { - "message": "Dela" - }, "movedItemToOrg": { "message": "$ITEMNAME$ flyttades till $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Klona" }, - "passwordGeneratorPolicyInEffect": { - "message": "En eller flera organisationspolicyer påverkar dina generatorinställningar." - }, "passwordGenerator": { "message": "Lösenordsgenerator" }, @@ -2334,6 +2324,9 @@ "message": "Domäner", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Exkluderade domäner" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Webbplats $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Sök bland Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Lägg till Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Det maximala antalet åtkomster har uppnåtts", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Utgången" }, - "pendingDeletion": { - "message": "Väntar på radering" - }, "passwordProtected": { "message": "Lösenordsskyddad" }, @@ -2472,24 +2462,9 @@ "message": "Redigera Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Vilken typ av Send är detta?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Ett vänligt namn som beskriver denna Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Filen du vill skicka." - }, "deletionDate": { "message": "Raderingsdatum" }, - "deletionDateDesc": { - "message": "Denna Send kommer att raderas permanent på angivet datum och klockslag.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Utgångsdatum" }, - "expirationDateDesc": { - "message": "Om angiven kommer åtkomst till denna Send att upphöra på angivet datum och tid.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 dag" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Anpassad" }, - "maximumAccessCount": { - "message": "Maximalt antal åtkomster" - }, - "maximumAccessCountDesc": { - "message": "Om angivet kommer användare inte längre kunna komma åt denna Send när det maximala antalet åtkomster har uppnåtts.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Kräv valfritt ett lösenord för att användare ska komma åt denna Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Privata anteckningar om denna Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Inaktivera denna Send så att ingen kan komma åt den.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kopiera länken till denna Send vid sparande.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Texten du vill skicka." - }, - "sendHideText": { - "message": "Dölj texten för denna Send som standard", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Nuvarande antal åtkomster" - }, "createSend": { "message": "Ny Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Innan du börjar" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "För att använda en datumväljare med kalenderstil", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "klicka här", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "för att öppna ett nytt fönster.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Det angivna utgångsdatumet är inte giltigt." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Det gick inte att spara raderings- och utgångsdatum." }, - "hideEmail": { - "message": "Dölj min e-postadress för mottagare." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "En eller flera organisationsriktlinjer påverkar dina Send-inställningar." - }, "passwordPrompt": { "message": "Återupprepa huvudlösenord" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Fel" }, - "regenerateUsername": { - "message": "Återskapa användarnamn" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generera användarnamn" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Typ av användarnamn" - }, "plusAddressedEmail": { "message": "Plusadresserad e-post", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Webbplatsnamn" }, - "whatWouldYouLikeToGenerate": { - "message": "Vad skulle du vilja generera?" - }, - "passwordType": { - "message": "Lösenordstyp" - }, "service": { "message": "Tjänst" }, @@ -3503,38 +3425,6 @@ "message": "Växla synlig/dold", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Importera din data till Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Skydda din LastPass-data och importera till Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Spara som okrypterad fil", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Importera till Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importerar...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data har importerats till ditt valv!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Fel vid importeringen. Kolla konsolen för detaljer.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Nätverksfel uppstod vid import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Aliasdomän" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden har fått ett nytt utseende!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Kontoåtgärder" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 8f7673803cb..a7913bbfc9b 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Length" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "Uppercase (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum special" }, - "avoidAmbChar": { - "message": "Avoid ambiguous characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "List identity items on the Tab page for easy autofill." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Clear clipboard", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Move to organization" }, - "share": { - "message": "Share" - }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Clone" }, - "passwordGeneratorPolicyInEffect": { - "message": "One or more organization policies are affecting your generator settings." - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Search Sends", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Add Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Text" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "Edit Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 day" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "The text you want to send." - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 2d65bb40f1a..a8a2585f42f 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -454,9 +454,6 @@ "length": { "message": "ความยาว" }, - "passwordMinLength": { - "message": "Minimum password length" - }, "uppercase": { "message": "ตัวพิมพ์ใหญ่ (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Minimum Special" }, - "avoidAmbChar": { - "message": "Avoid Ambiguous Characters", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "แสดงรายการข้อมูลประจำตัวในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย" }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "ล้างคลิปบอร์ด", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "ย้ายไปยังแบบองค์กร" }, - "share": { - "message": "แชร์" - }, "movedItemToOrg": { "message": "ย้าย $ITEMNAME$ ไปยัง $ORGNAME$ แล้ว", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "โคลน" }, - "passwordGeneratorPolicyInEffect": { - "message": "นโยบายองค์กรอย่างน้อยหนึ่งนโยบายส่งผลต่อการตั้งค่าตัวสร้างของคุณ" - }, "passwordGenerator": { "message": "Password generator" }, @@ -2334,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "ค้นหาใน Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "เพิ่ม Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "ข้อความ" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Max access count reached", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Expired" }, - "pendingDeletion": { - "message": "Pending deletion" - }, "passwordProtected": { "message": "Password protected" }, @@ -2472,24 +2462,9 @@ "message": "แก้ไข Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "What type of Send is this?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "A friendly name to describe this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "The file you want to send." - }, "deletionDate": { "message": "Deletion date" }, - "deletionDateDesc": { - "message": "The Send will be permanently deleted on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Expiration date" }, - "expirationDateDesc": { - "message": "If set, access to this Send will expire on the specified date and time.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 วัน" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Custom" }, - "maximumAccessCount": { - "message": "Maximum Access Count" - }, - "maximumAccessCountDesc": { - "message": "If set, users will no longer be able to access this Send once the maximum access count is reached.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Private notes about this Send.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Copy this Send's link to clipboard upon save.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "ข้อความที่คุณต้องการส่ง" - }, - "sendHideText": { - "message": "Hide this Send's text by default.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Current access count" - }, "createSend": { "message": "New Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Before you start" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "The expiration date provided is not valid." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "There was an error saving your deletion and expiration dates." }, - "hideEmail": { - "message": "Hide my email address from recipients." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." - }, "passwordPrompt": { "message": "Master password re-prompt" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Error" }, - "regenerateUsername": { - "message": "Regenerate username" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Generate username" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Username type" - }, "plusAddressedEmail": { "message": "Plus addressed email", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Website name" }, - "whatWouldYouLikeToGenerate": { - "message": "What would you like to generate?" - }, - "passwordType": { - "message": "Password type" - }, "service": { "message": "Service" }, @@ -3503,38 +3425,6 @@ "message": "Toggle collapse", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Import your data to Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Save as unencrypted file", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Import to Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Importing...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Data successfully imported!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Error importing. Check console for details.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Network error encountered during import.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias domain" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Autofill suggestions" }, + "itemSuggestions": { + "message": "Suggested items" + }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 4c87d5189fe..34ad0f14483 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -385,7 +385,7 @@ "message": "Hiç klasör eklenmedi" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "Kasanızdaki kayıtları organize etmek için klasörler oluşturun" }, "deleteFolderPermanently": { "message": "Bu klasörü kalıcı olarak silmek istediğinizden emin misiniz?" @@ -454,9 +454,6 @@ "length": { "message": "Uzunluk" }, - "passwordMinLength": { - "message": "Minimum parola uzunluğu" - }, "uppercase": { "message": "Büyük harf (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "En az özel karakter" }, - "avoidAmbChar": { - "message": "Okurken karışabilecek karakterleri kullanma", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Okurken karışabilecek karakterleri kullanma", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Kolay otomatik doldurma için sekme sayfasında kimlikleri listele." }, + "clickToAutofillOnVault": { + "message": "Kasa görünümünde otomatik doldurmak istediğiniz kayıtlara tıklayın" + }, "clearClipboard": { "message": "Panoyu temizle", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Kuruluşa taşı" }, - "share": { - "message": "Paylaş" - }, "movedItemToOrg": { "message": "$ITEMNAME$ $ORGNAME$ kuruluşuna taşındı", "placeholders": { @@ -1282,7 +1275,7 @@ "message": "Bitwarden'ı desteklediğiniz için teşekkür ederiz." }, "premiumFeatures": { - "message": "Premium'a yükseltin ve şunları alın:" + "message": "Premium'a geçmenin avantajları:" }, "premiumPrice": { "message": "Bunların hepsi sadece yılda $PRICE$!", @@ -1509,13 +1502,13 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "enableAutoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "enableAutoFillOnPageLoadDesc": { - "message": "Sayfa yüklendiğinde giriş formu tespit edilirse otomatik olarak formu doldur." + "message": "Sayfa yüklenince giriş formu tespit edilirse otomatik olarak formu doldur." }, "experimentalFeature": { "message": "Ele geçirilmiş veya güvenilmeyen web siteleri sayfa yüklenirken otomatik doldurmayı suistimal edebilir." @@ -1530,19 +1523,19 @@ "message": "Hesaplar için varsayılan otomatik doldurma ayarı" }, "defaultAutoFillOnPageLoadDesc": { - "message": "\"Sayfa yüklendiğinde otomatik doldur\"u her hesabın \"Düzenle\" görünümünden ayrı ayrı kapatabilirsiniz." + "message": "\"Sayfa yüklenince otomatik doldur\"u her hesabın \"Düzenle\" görünümünden ayrı ayrı kapatabilirsiniz." }, "itemAutoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur (Seçeneklerde ayarlanmışsa)" + "message": "Sayfa yüklenince otomatik doldur (Seçeneklerde ayarlanmışsa)" }, "autoFillOnPageLoadUseDefault": { "message": "Varsayılan ayarı kullan" }, "autoFillOnPageLoadYes": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "autoFillOnPageLoadNo": { - "message": "Sayfa yüklendiğinde otomatik doldurma" + "message": "Sayfa yüklenince otomatik doldurma" }, "commandOpenPopup": { "message": "Kasayı açılır pencerede aç" @@ -2047,9 +2040,6 @@ "clone": { "message": "Klonla" }, - "passwordGeneratorPolicyInEffect": { - "message": "Bir ya da daha fazla kuruluş ilkesi, oluşturucu ayarlarınızı etkiliyor." - }, "passwordGenerator": { "message": "Parola üreteci" }, @@ -2334,6 +2324,9 @@ "message": "Alan adları", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Engellenen alan adları" + }, "excludedDomains": { "message": "Hariç tutulan alan adları" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, oturum açmış tüm hesaplar için bu alan adlarının hesap bilgilerini kaydetmeyi sormayacaktır. Değişikliklerin etkili olması için sayfayı yenilemeniz gerekir." }, + "blockedDomainsDesc": { + "message": "Bu siteler için otomatik doldurma ve diğer ilgili özellikler önerilmeyecektir. Değişikliklerin devreye girmesi için sayfayı yenilemelisiniz." + }, + "autofillBlockedNoticeV2": { + "message": "Bu sitede otomatik doldurma engellenmiş." + }, + "autofillBlockedNoticeGuidance": { + "message": "Bunu ayarlardan değiştirebilirsiniz" + }, "websiteItemLabel": { "message": "Web sitesi $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Engelli alan adı değişiklikleri kaydedildi" + }, "excludedDomainsSavedSuccess": { "message": "Alan adı istisnası değişiklikleri kaydedildi" }, @@ -2389,14 +2394,6 @@ "message": "Send ayrıntıları", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Send'lerde ara", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Send ekle", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Metin" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Metni varsayılan olarak gizle" }, - "maxAccessCountReached": { - "message": "Maksimum erişim sayısına ulaşıldı", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Süresi dolmuş" }, - "pendingDeletion": { - "message": "Silinmesi bekleniyor" - }, "passwordProtected": { "message": "Parola korumalı" }, @@ -2472,24 +2462,9 @@ "message": "Send'i düzenle", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Bu ne tür bir Send?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Bu Send'i açıklayan anlaşılır bir ad", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Göndermek istediğiniz dosya." - }, "deletionDate": { "message": "Silinme tarihi" }, - "deletionDateDesc": { - "message": "Bu Send belirtilen tarih ve saatte kalıcı olacak silinecek.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Bu Send belirtilen tarihte kalıcı olacak silinecek.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Son kullanma tarihi" }, - "expirationDateDesc": { - "message": "Bunu ayarlarsanız belirtilen tarih ve saatten sonra bu Send'e erişilemeyecektir.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 gün" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Özel" }, - "maximumAccessCount": { - "message": "Maksimum erişim sayısı" - }, - "maximumAccessCountDesc": { - "message": "Bunu ayarlarsanız maksimum erişim sayısına ulaşıldıktan sonra bu Send'e erişilemeyecektir.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Kullanıcıların bu Send'e erişmek için parola girmelerini isteyebilirsiniz.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Alıcıların bu Send'e erişmesi için isterseniz parola ekleyebilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Bu Send ile ilgili özel notlar.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Kimsenin erişememesi için bu Send'i devre dışı bırak.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Kaydettikten sonra bu Send'in linkini panoya kopyala.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Göndermek istediğiniz metin." - }, - "sendHideText": { - "message": "Bu Send'in metnini varsayılan olarak gizle.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Mevcut erişim sayısı" - }, "createSend": { "message": "Yeni Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Başlamadan önce" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Takvim tarzı tarih seçiyi kullanmak için", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "buraya tıklayarak", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "yeni bir pencere açın.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Belirtilen son kullanma tarihi geçersiz." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Silinme ve son kullanma tarihleriniz kaydedilirken bir hata oluştu." }, - "hideEmail": { - "message": "E-posta adresimi alıcılardan gizle." - }, "hideYourEmail": { "message": "E-posta adresimi Send'i görüntüleyenlerden gizle." }, - "sendOptionsPolicyInEffect": { - "message": "Bir veya daha fazla kuruluş ilkesi Send seçeneklerinizi etkiliyor." - }, "passwordPrompt": { "message": "Ana parolayı yeniden iste" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Hata" }, - "regenerateUsername": { - "message": "Kullanıcı adını yeniden oluştur" + "decryptionError": { + "message": "Şifre çözme sorunu" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Kullanıcı adı oluştur" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Kullanıcı adı türü" - }, "plusAddressedEmail": { "message": "Artı adresli e-posta", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Web sitesi adı" }, - "whatWouldYouLikeToGenerate": { - "message": "Ne oluşturmak istersiniz?" - }, - "passwordType": { - "message": "Parola türü" - }, "service": { "message": "Servis" }, @@ -3229,7 +3151,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Kuruluş ilkeleriniz, sayfa yüklendiğinde otomatik doldurmayı etkinleştirdi." + "message": "Kuruluş ilkeleriniz, sayfa yüklenince otomatik doldurmayı etkinleştirdi." }, "howToAutofill": { "message": "Otomatik doldurma nasıl yapılır?" @@ -3503,47 +3425,15 @@ "message": "Daraltmayı aç/kapat", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Verileriniz Bitwarden'a aktarılsın mı?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "LastPass verileriniz korunsun ve Bitwarden'a aktarılsın mı?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Şifrelenmemiş dosya olarak kaydet", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Bitwarden'a aktar", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "İçe aktarılıyor...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Veriler başarıyla içe aktarıldı!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "İçe aktarma hatası. Ayrıntılar için konsolu kontrol edin.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "İçe aktarma sırasında ağ hatasıyla karşılaşıldı.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Alias alan adı" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Ana parolayı yeniden isteyen kayıtlar sayfa yüklendiğinde otomatik olarak doldurulamaz. Sayfa yüklendiğinde otomatik doldurma kapatıldı.", + "message": "Ana parolayı yeniden isteyen kayıtlar sayfa yüklenince otomatik olarak doldurulamaz. Sayfa yüklenince otomatik doldurma kapatıldı.", "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Sayfa yüklendiğinde otomatik doldurma, varsayılan ayarı kullanacak şekilde ayarlandı.", + "message": "Sayfa yüklenince otomatik doldurma, varsayılan ayarı kullanacak şekilde ayarlandı.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { @@ -3818,7 +3708,7 @@ "message": "Geçiş anahtarı" }, "accessing": { - "message": "Erişim" + "message": "Erişilen konum:" }, "loggedInExclamation": { "message": "Giriş yapıldı!" @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Önerileri otomatik doldur" }, + "itemSuggestions": { + "message": "Önerilen kayıtlar" + }, "autofillSuggestionsTip": { "message": "Otomatik doldurma için bu siteye ait bir hesap kaydededin" }, @@ -4261,7 +4154,7 @@ "message": "Ek bilgiler" }, "itemHistory": { - "message": "Öğe geçmişi" + "message": "Kayıt geçmişi" }, "lastEdited": { "message": "Son düzenlenme" @@ -4411,7 +4304,7 @@ } }, "autoFillOnPageLoad": { - "message": "Sayfa yüklendiğinde otomatik doldur" + "message": "Sayfa yüklenince otomatik doldur" }, "cardExpiredTitle": { "message": "Kartın süresi dolmuş" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Metin Send'leri" }, - "bitwardenNewLook": { - "message": "Bitwarden'ın tasarımı güncellendi!" - }, - "bitwardenNewLookDesc": { - "message": "Otomatik doldurma ve kasanızda arama yapma artık eskisinden daha kolay. Yeni tasarıma göz atmayı unutmayın!" - }, "accountActions": { "message": "Hesap işlemleri" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Bu kaydı düzenleme yetkisine sahip değilsiniz" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Kimlik doğrulanıyor" }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index af8f6668111..a1ec52ee80e 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Довжина" }, - "passwordMinLength": { - "message": "Мінімальна довжина пароля" - }, "uppercase": { "message": "Верхній регістр (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Мінімум спеціальних символів" }, - "avoidAmbChar": { - "message": "Уникати неоднозначних символів", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Уникати неоднозначних символів", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Показувати список посвідчень на сторінці вкладки для легкого автозаповнення." }, + "clickToAutofillOnVault": { + "message": "Натисніть на запис у режимі перегляду сховища для автозаповнення" + }, "clearClipboard": { "message": "Очистити буфер обміну", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Перемістити до організації" }, - "share": { - "message": "Поділитися" - }, "movedItemToOrg": { "message": "$ITEMNAME$ переміщено до $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Клонувати" }, - "passwordGeneratorPolicyInEffect": { - "message": "На параметри генератора впливають одна чи декілька політик організації." - }, "passwordGenerator": { "message": "Генератор паролів" }, @@ -2334,6 +2324,9 @@ "message": "Домени", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Заблоковані домени" + }, "excludedDomains": { "message": "Виключені домени" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не запитуватиме про збереження даних входу для цих доменів для всіх облікових записів, до яких виконано вхід. Потрібно оновити сторінку для застосування змін." }, + "blockedDomainsDesc": { + "message": "Автозаповнення та інші пов'язані функції не пропонуватимуться для цих вебсайтів. Вам слід оновити сторінку для застосування змін." + }, + "autofillBlockedNoticeV2": { + "message": "Автозаповнення для цього вебсайту заблоковано." + }, + "autofillBlockedNoticeGuidance": { + "message": "Змінити в налаштуваннях" + }, "websiteItemLabel": { "message": "Вебсайт $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Зміни заблокованих доменів збережено" + }, "excludedDomainsSavedSuccess": { "message": "Виняток для домену збережено" }, @@ -2389,14 +2394,6 @@ "message": "Надіслати подробиці", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Шукати відправлення", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Додати відправлення", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Текст" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Типово приховувати текст" }, - "maxAccessCountReached": { - "message": "Досягнуто максимальної кількості доступів", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Термін дії завершився" }, - "pendingDeletion": { - "message": "Очікується видалення" - }, "passwordProtected": { "message": "Захищено паролем" }, @@ -2472,24 +2462,9 @@ "message": "Редагування", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Який це тип відправлення?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Опис цього відправлення.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Файл, який ви хочете відправити." - }, "deletionDate": { "message": "Термін дії" }, - "deletionDateDesc": { - "message": "Відправлення буде остаточно видалено у вказаний час.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "Відправлення буде остаточно видалено у вказану дату.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Дата завершення" }, - "expirationDateDesc": { - "message": "Якщо встановлено, термін дії цього відправлення завершиться у вказаний час.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 день" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Власний" }, - "maximumAccessCount": { - "message": "Максимальна кількість доступів" - }, - "maximumAccessCountDesc": { - "message": "Якщо встановлено, користувачі більше не зможуть отримати доступ до цього відправлення після досягнення максимальної кількості доступів.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Ви можете встановити пароль для доступу до цього відправлення.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "За бажання додайте пароль для отримувачів цього відправлення.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Особисті нотатки про це відправлення.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Деактивувати це відправлення для скасування доступу до нього.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Копіювати посилання цього відправлення перед збереженням.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Текст, який ви хочете відправити." - }, - "sendHideText": { - "message": "Приховувати текст цього відправлення.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Поточна кількість доступів" - }, "createSend": { "message": "Нове відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Перед початком" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Щоб використовувати календарний стиль вибору дати", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "натисніть тут", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "щоб відкріпити ваше вікно.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Вказано недійсний термін дії." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "При збереженні дат видалення і терміну дії виникла помилка." }, - "hideEmail": { - "message": "Приховувати мою адресу електронної пошти від отримувачів." - }, "hideYourEmail": { "message": "Приховати адресу е-пошти від отримувачів." }, - "sendOptionsPolicyInEffect": { - "message": "На параметри відправлень впливають одна чи декілька політик організації." - }, "passwordPrompt": { "message": "Повторний запит головного пароля" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Помилка" }, - "regenerateUsername": { - "message": "Повторно генерувати ім'я користувача" + "decryptionError": { + "message": "Помилка розшифрування" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden не зміг розшифрувати вказані нижче елементи сховища." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Зверніться до служби підтримки клієнтів,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "щоб уникнути втрати даних.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Генерувати ім'я користувача" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Тип імені користувача" - }, "plusAddressedEmail": { "message": "Адреса е-пошти з плюсом", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Назва вебсайту" }, - "whatWouldYouLikeToGenerate": { - "message": "Що ви бажаєте згенерувати?" - }, - "passwordType": { - "message": "Тип пароля" - }, "service": { "message": "Послуга" }, @@ -3503,38 +3425,6 @@ "message": "Згорнути/розгорнути", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Імпортувати ваші дані до Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Захистити ваші дані LastPass та імпортувати до Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Зберегти незашифрований файл", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Імпортувати до Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Триває імпортування...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Дані успішно імпортовано!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Помилка імпортування. Перевірте консоль для перегляду подробиць.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Під час імпортування сталася мережева помилка.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Псевдонім домену" }, @@ -4088,6 +3978,9 @@ "autofillSuggestions": { "message": "Пропозиції автозаповнення" }, + "itemSuggestions": { + "message": "Запропоновані записи" + }, "autofillSuggestionsTip": { "message": "Зберегти дані входу цього сайту для автозаповнення" }, @@ -4664,12 +4557,6 @@ "textSends": { "message": "Відправлення тексту" }, - "bitwardenNewLook": { - "message": "Bitwarden має новий вигляд!" - }, - "bitwardenNewLookDesc": { - "message": "Ще простіше автозаповнення та інтуїтивніший пошук у сховищі. Ознайомтеся!" - }, "accountActions": { "message": "Дії з обліковим записом" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "Вам не дозволено редагувати цей запис" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Біометричне розблокування недоступне, оскільки спочатку потрібно ввести PIN-код або пароль." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Біометричне розблокування наразі недоступне." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Біометричне розблокування недоступне через неправильно налаштовані системні файли." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Біометричне розблокування недоступне, оскільки програму Bitwarden для комп'ютера закрито." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Біометричне розблокування недоступне, оскільки воно не увімкнене для $EMAIL$ у програмі Bitwarden для комп'ютера.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Біометричне розблокування зараз недоступне з невідомої причини." + }, "authenticating": { "message": "Аутентифікація" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index db877e318ef..b6668d8dc99 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -454,9 +454,6 @@ "length": { "message": "Độ dài" }, - "passwordMinLength": { - "message": "Độ dài mật khẩu tối thiểu" - }, "uppercase": { "message": "Chữ in hoa (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "Số kí tự đặc biệt tối thiểu" }, - "avoidAmbChar": { - "message": "Tránh các ký tự không rõ ràng", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "Avoid ambiguous characters", "description": "Label for the avoid ambiguous characters checkbox." @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "Liệt kê các mục danh tính trên trang Tab để dễ dàng tự động điền." }, + "clickToAutofillOnVault": { + "message": "Click items to autofill on Vault view" + }, "clearClipboard": { "message": "Dọn dẹp khay nhớ tạm", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "Di chuyển đến tổ chức" }, - "share": { - "message": "Chia sẻ" - }, "movedItemToOrg": { "message": "$ITEMNAME$ đã được di chuyển đến $ORGNAME$", "placeholders": { @@ -2047,9 +2040,6 @@ "clone": { "message": "Tạo bản sao" }, - "passwordGeneratorPolicyInEffect": { - "message": "Các chính sách của tổ chức đang ảnh hưởng đến cài đặt tạo mật khẩu của bạn." - }, "passwordGenerator": { "message": "Trình tạo mật khẩu" }, @@ -2334,6 +2324,9 @@ "message": "Các tên miền", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Tên miền đã loại trừ" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden sẽ không yêu cầu lưu thông tin đăng nhập cho các miền này. Bạn phải làm mới trang để các thay đổi có hiệu lực." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." + }, + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" + }, "websiteItemLabel": { "message": "Trang Web $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Các thay đổi tên miền loại trừ đã được lưu" }, @@ -2389,14 +2394,6 @@ "message": "Send details", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "Tìm kiếm mục Gửi", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "Thêm mục Gửi", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "Văn bản" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "Hide text by default" }, - "maxAccessCountReached": { - "message": "Đã vượt số lần truy cập tối đa", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "Đã hết hạn" }, - "pendingDeletion": { - "message": "Đang chờ xóa" - }, "passwordProtected": { "message": "Mật khẩu đã được bảo vệ" }, @@ -2472,24 +2462,9 @@ "message": "Sửa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "Đây là loại Send gì?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "Một cái tên thân thiện để mô tả về Send này.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "Tập tin bạn muốn gửi." - }, "deletionDate": { "message": "Ngày xóa" }, - "deletionDateDesc": { - "message": "Mục Gửi sẽ được xóa vĩnh viễn vào ngày và giờ chỉ định.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "The Send will be permanently deleted on this date.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "Ngày hết hạn" }, - "expirationDateDesc": { - "message": "Nếu được thiết lập, mục Gửi này sẽ hết hạn vào ngày và giờ được chỉ định.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 ngày" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "Tùy chỉnh" }, - "maximumAccessCount": { - "message": "Số lượng truy cập tối đa" - }, - "maximumAccessCountDesc": { - "message": "Nếu được thiết lập, khi đã đạt tới số lượng truy cập tối đa, người dùng sẽ không thể truy cập mục Gửi này nữa.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "Yêu cầu nhập mật khẩu khi người dùng truy cập vào phần Gửi này.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "Ghi chú riêng tư về Send này.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "Vô hiệu hoá mục Gửi này để không ai có thể truy cập nó.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "Sao chép liên kết của Send này vào khay nhớ tạm khi lưu.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "Văn bản bạn muốn gửi." - }, - "sendHideText": { - "message": "Ẩn văn bản của Send này theo mặc định.", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "Số lượng truy cập hiện tại" - }, "createSend": { "message": "Send mới", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "Trước khi bạn bắt đầu" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "Để dùng bộ chọn ngày dạng lịch", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "nhấn vào đây", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "để bật cửa sổ của bạn ra.", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "Ngày hết hạn bạn nhập không hợp lệ." }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "Đã xảy ra lỗi khi lưu ngày xoá và ngày hết hạn của bạn." }, - "hideEmail": { - "message": "Ẩn địa chỉ email của tôi khỏi người nhận." - }, "hideYourEmail": { "message": "Hide your email address from viewers." }, - "sendOptionsPolicyInEffect": { - "message": "Các chính sách của tổ chức đang ảnh hưởng đến tùy chọn Gửi của bạn." - }, "passwordPrompt": { "message": "Nhắc lại mật khẩu chính" }, @@ -2884,8 +2804,19 @@ "error": { "message": "Lỗi" }, - "regenerateUsername": { - "message": "Tạo lại tên người dùng" + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "Tạo tên người dùng" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "Loại tên người dùng" - }, "plusAddressedEmail": { "message": "Địa chỉ email có hậu tố", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "Tên website" }, - "whatWouldYouLikeToGenerate": { - "message": "Bạn muốn tạo gì?" - }, - "passwordType": { - "message": "Loại mật khẩu" - }, "service": { "message": "Dịch vụ" }, @@ -3503,38 +3425,6 @@ "message": "Bật/tắt thu gọn", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "Nhập dữ liệu của bạn vào Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "Bảo vệ dữ liệu LastPass của bạn và nhập vào Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "Lưu dưới dạng tập tin không được mã hóa", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "Nhập vào Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "Đang nhập...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "Dữ liệu đã được nhập thành công!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "Xảy ra lỗi trong quá trình nhập. Kiểm tra bảng điều khiển để biết thêm chi tiết.", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "Đã xảy ra lỗi mạng trong quá trình nhập dữ liệu.", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "Tên miền thay thế" }, @@ -4086,7 +3976,10 @@ "message": "Đã xóa mã khoá" }, "autofillSuggestions": { - "message": "Gợi ý điền tự động" + "message": "Autofill suggestions" + }, + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Lưu thông tin đăng nhập cho trang này để tự động điền" @@ -4664,12 +4557,6 @@ "textSends": { "message": "Text Sends" }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" - }, "accountActions": { "message": "Account actions" }, @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, @@ -4911,10 +4825,10 @@ "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Thông báo quan trọng" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Thiết lập đăng nhập hai bước" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." @@ -4923,7 +4837,7 @@ "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." }, "remindMeLater": { - "message": "Remind me later" + "message": "Nhắc sau" }, "newDeviceVerificationNoticePageOneFormContent": { "message": "Do you have reliable access to your email, $EMAIL$?", @@ -4935,16 +4849,16 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Không, tôi không có" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "Có, tôi có quyền truy cập email này" }, "turnOnTwoStepLogin": { "message": "Turn on two-step login" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Đổi email tài khoản" }, "extensionWidth": { "message": "Extension width" diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index a738ea4bb70..8cd91d44a8a 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", + "message": "无论是在家中、工作中还是在旅途中,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -50,7 +50,7 @@ "message": "提交" }, "emailAddress": { - "message": "电子邮件地址" + "message": "电子邮箱地址" }, "masterPass": { "message": "主密码" @@ -239,7 +239,7 @@ "message": "添加项目" }, "accountEmail": { - "message": "账户邮件地址" + "message": "账户电子邮箱" }, "requestHint": { "message": "请求提示" @@ -248,13 +248,13 @@ "message": "请求密码提示" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "输入您的账户电子邮件地址,您的密码提示将发送给您" + "message": "输入您的账户电子邮箱地址,您的密码提示将发送给您" }, "passwordHint": { "message": "密码提示" }, "enterEmailToGetHint": { - "message": "请输入您账户的电子邮件地址来接收主密码提示。" + "message": "请输入您的账户电子邮箱地址来接收主密码提示。" }, "getMasterPasswordHint": { "message": "获取主密码提示" @@ -284,13 +284,13 @@ "message": "前往网页 App 吗?" }, "continueToWebAppDesc": { - "message": "在网页应用上探索 Bitwarden 账户的更多功能。" + "message": "在网页 App 上探索 Bitwarden 账户的更多功能。" }, "continueToHelpCenter": { "message": "前往帮助中心吗?" }, "continueToHelpCenterDesc": { - "message": "访问帮助中心了解更多如何使用 Bitwarden 的信息。" + "message": "在帮助中心进一步了解如何使用 Bitwarden。" }, "continueToBrowserExtensionStore": { "message": "前往浏览器扩展商店吗?" @@ -299,7 +299,7 @@ "message": "帮助别人了解 Bitwarden 是否适合他们。立即访问浏览器的扩展程序商店并留下评分。" }, "changeMasterPasswordOnWebConfirmation": { - "message": "您可以在 Bitwarden 网页应用上更改您的主密码。" + "message": "您可以在 Bitwarden 网页 App 上更改您的主密码。" }, "fingerprintPhrase": { "message": "指纹短语", @@ -379,7 +379,7 @@ "message": "文件夹名称" }, "folderHintText": { - "message": "通过在父文件夹名后面跟随一个「/」来嵌套文件夹。例如:Social/Forums" + "message": "通过在父文件夹名后面跟随「/」来嵌套文件夹。示例:Social/Forums" }, "noFoldersAdded": { "message": "未添加文件夹" @@ -428,7 +428,7 @@ "description": "Short for 'credential generator'." }, "passGenInfo": { - "message": "自动生成安全可靠唯一的登录密码。" + "message": "自动为您的登录生成强大且唯一的密码。" }, "bitWebVaultApp": { "message": "Bitwarden 网页 App" @@ -454,9 +454,6 @@ "length": { "message": "长度" }, - "passwordMinLength": { - "message": "最小密码长度" - }, "uppercase": { "message": "大写 (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -528,10 +525,6 @@ "minSpecial": { "message": "符号最少个数" }, - "avoidAmbChar": { - "message": "避免易混淆的字符", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { "message": "避免易混淆的字符", "description": "Label for the avoid ambiguous characters checkbox." @@ -604,7 +597,7 @@ "message": "前往" }, "launchWebsite": { - "message": "启动网站" + "message": "打开网站" }, "launchWebsiteName": { "message": "前往 $ITEMNAME$ 的网站", @@ -758,10 +751,10 @@ "message": "发生了一个错误" }, "emailRequired": { - "message": "必须填写电子邮件地址。" + "message": "必须填写电子邮箱地址。" }, "invalidEmail": { - "message": "无效的电子邮件地址。" + "message": "无效的电子邮箱地址。" }, "masterPasswordRequired": { "message": "必须填写主密码。" @@ -770,7 +763,7 @@ "message": "必须填写确认主密码。" }, "masterPasswordMinlength": { - "message": "主密码必须至少 $VALUE$ 个字符长度。", + "message": "主密码长度必须至少为 $VALUE$ 个字符。", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -798,7 +791,7 @@ "message": "您可以关闭此窗口" }, "masterPassSent": { - "message": "我们已经为您发送了包含主密码提示的邮件。" + "message": "我们已经为您发送了包含主密码提示的电子邮件。" }, "verificationCodeRequired": { "message": "必须填写验证码。" @@ -820,7 +813,7 @@ } }, "autofillError": { - "message": "无法在此页面上自动填充所选项目。请改为手动复制并粘贴。" + "message": "无法在此页面上自动填充所选项目。请手动复制并粘贴。" }, "totpCaptureError": { "message": "无法从当前网页扫描二维码" @@ -841,7 +834,7 @@ "message": "Bitwarden 可以存储并填充两步验证码。选择相机图标来截取此网站的验证器二维码,或者手动复制并粘贴密钥到此字段。" }, "learnMoreAboutAuthenticators": { - "message": "了解更多关于验证器的信息" + "message": "进一步了解验证器" }, "copyTOTP": { "message": "复制验证器密钥 (TOTP)" @@ -862,13 +855,13 @@ "message": "登录到 Bitwarden" }, "restartRegistration": { - "message": "重新开始注册" + "message": "重启注册" }, "expiredLink": { "message": "失效链接" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "请重新注册或尝试登录。" + "message": "请重启注册或尝试登录。" }, "youMayAlreadyHaveAnAccount": { "message": "您可能已经有一个账户了" @@ -895,7 +888,7 @@ "message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件)来验证您的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?" }, "twoStepLoginConfirmationContent": { - "message": "通过在 Bitwarden 网页 App 中设置两步登录,可以使您的账户更加安全。" + "message": "在 Bitwarden 网页 App 中设置两步登录,让您的账户更加安全。" }, "twoStepLoginConfirmationTitle": { "message": "前往网页 App 吗?" @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "在标签页上列出身份项目,以便于自动填充。" }, + "clickToAutofillOnVault": { + "message": "在密码库视图中点击项目以自动填充" + }, "clearClipboard": { "message": "清空剪贴板", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1075,7 +1071,7 @@ "message": "主题" }, "themeDesc": { - "message": "更改本应用程序的颜色主题。" + "message": "更改应用程序的颜色主题。" }, "themeDescAlt": { "message": "更改应用程序的颜色主题。适用于所有已登录的账户。" @@ -1137,7 +1133,7 @@ "message": "确认密码库导出" }, "exportWarningDesc": { - "message": "导出的密码库数据包含未加密格式。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出的文件。用完后请立即将其删除。" + "message": "此导出包含未加密格式的密码库数据。您不应该通过不安全的渠道(例如电子邮件)来存储或发送此导出文件。使用完后请立即将其删除。" }, "encExportKeyWarningDesc": { "message": "此导出将使用您账户的加密密钥来加密您的数据。如果您曾经轮换过账户的加密密钥,您应将其重新导出,否则您将无法解密导出的文件。" @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "移动到组织" }, - "share": { - "message": "共享" - }, "movedItemToOrg": { "message": "$ITEMNAME$ 已移动到 $ORGNAME$", "placeholders": { @@ -1330,7 +1323,7 @@ "message": "身份验证会话超时。请重新启动登录过程。" }, "enterVerificationCodeEmail": { - "message": "请输入发送给电子邮件 $EMAIL$ 的 6 位数验证码。", + "message": "请输入发送给 $EMAIL$ 的 6 位数验证码。", "placeholders": { "email": { "content": "$1", @@ -1407,7 +1400,7 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "为您的组织使用 Duo Security 的 Duo 移动应用、短信、电话或 U2F 安全钥匙来进行验证。", + "message": "为您的组织使用 Duo Security 的 Duo 移动 App、短信、电话或 U2F 安全钥匙来进行验证。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { @@ -1417,7 +1410,7 @@ "message": "使用任何 WebAuthn 兼容的安全钥匙访问您的帐户。" }, "emailTitle": { - "message": "电子邮件" + "message": "电子邮箱" }, "emailDescV2": { "message": "输入发送到您的电子邮箱的代码。" @@ -1435,7 +1428,7 @@ "message": "对于高级配置,您可以单独指定每个服务的基础 URL。" }, "selfHostedEnvFormInvalid": { - "message": "您必须添加基础服务器 URL 或至少添加一个自定义环境。" + "message": "您必须添加基础服务器 URL 或至少一个自定义环境。" }, "customEnvironment": { "message": "自定义环境" @@ -1521,10 +1514,10 @@ "message": "不完整或不信任的网站可以利用页面加载时的自动填充功能。" }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "了解更多关于风险的信息" + "message": "进一步了解风险" }, "learnMoreAboutAutofill": { - "message": "了解更多关于自动填充的信息" + "message": "进一步了解自动填充" }, "defaultAutoFillOnPageLoad": { "message": "登录项目的默认自动填充设置" @@ -1727,7 +1720,7 @@ "message": "许可证号码" }, "email": { - "message": "电子邮件" + "message": "电子邮箱" }, "phone": { "message": "电话" @@ -2047,9 +2040,6 @@ "clone": { "message": "克隆" }, - "passwordGeneratorPolicyInEffect": { - "message": "一个或多个组织策略正在影响您的生成器设置。" - }, "passwordGenerator": { "message": "密码生成器" }, @@ -2133,7 +2123,7 @@ "message": "您仍然想要填充此登录信息吗?" }, "autofillIframeWarning": { - "message": "该表单由不同于您保存的登录的 URI 域名托管。选择「确定」以自动填充,或选择「取消」停止填充。" + "message": "该表单由与您保存的登录 URI 不同的域名托管。选择「确定」继续自动填充,或选择「取消」停止自动填充。" }, "autofillIframeWarningTip": { "message": "要防止以后出现此警告,请将此站点的 URI $HOSTNAME$ 保存到您的 Bitwarden 登录项目中。", @@ -2205,13 +2195,13 @@ "message": "取消订阅" }, "atAnyTime": { - "message": "随时" + "message": "随时。" }, "byContinuingYouAgreeToThe": { "message": "若继续,代表您同意" }, "and": { - "message": "以及" + "message": "和" }, "acceptPolicies": { "message": "选中此框表示您同意:" @@ -2328,12 +2318,15 @@ "message": "一个组织策略正影响您的所有权选项。" }, "personalOwnershipPolicyInEffectImports": { - "message": "组织策略已阻止将项目导入您的个人密码库。" + "message": "某个组织策略已阻止将项目导入您的个人密码库。" }, "domainsTitle": { "message": "域名", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "屏蔽域名" + }, "excludedDomains": { "message": "排除域名" }, @@ -2341,7 +2334,16 @@ "message": "Bitwarden 将不会询问是否为这些域名保存登录信息。您必须刷新页面才能使更改生效。" }, "excludedDomainsDescAlt": { - "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。必须刷新页面才能使更改生效。" + "message": "Bitwarden 将不会询问是否为所有已登录账户的这些域名保存登录信息。您必须刷新页面才能使更改生效。" + }, + "blockedDomainsDesc": { + "message": "将不会为这些网站提供自动填充和其他相关功能。您必须刷新页面才能使更改生效。" + }, + "autofillBlockedNoticeV2": { + "message": "该网站的自动填充已被屏蔽。" + }, + "autofillBlockedNoticeGuidance": { + "message": "在设置中更改它" }, "websiteItemLabel": { "message": "网站 $number$ (URI)", @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "屏蔽域名更改已保存" + }, "excludedDomainsSavedSuccess": { "message": "排除域名更改已保存" }, @@ -2389,14 +2394,6 @@ "message": "Send 详细信息", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "搜索 Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "添加 Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "文本" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "默认隐藏文本" }, - "maxAccessCountReached": { - "message": "已达最大访问次数", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "已过期" }, - "pendingDeletion": { - "message": "等待删除" - }, "passwordProtected": { "message": "密码保护" }, @@ -2472,24 +2462,9 @@ "message": "编辑 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "这是什么类型的 Send?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "用于描述此 Send 的友好名称。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "您想要发送的文件。" - }, "deletionDate": { "message": "删除日期" }, - "deletionDateDesc": { - "message": "此 Send 将在指定的日期和时间后被永久删除。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "此 Send 将在此日期后被永久删除。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "过期日期" }, - "expirationDateDesc": { - "message": "设置后,对此 Send 的访问将在指定的日期和时间后过期。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 天" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "自定义" }, - "maximumAccessCount": { - "message": "最大访问次数" - }, - "maximumAccessCountDesc": { - "message": "设置后,当达到最大访问次数时用户将不再能访问此 Send。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "可选,用户需要提供密码才能访问此 Send。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "添加一个用于收件人访问此 Send 的可选密码。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "关于此 Send 的私密备注。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "停用此 Send 则任何人无法访问它。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "保存时复制此 Send 的链接到剪贴板。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "您想要发送的文本。" - }, - "sendHideText": { - "message": "默认隐藏此 Send 的文本。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "当前访问次数" - }, "createSend": { "message": "创建 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "在开始之前" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "要使用日历样式的日期选择器", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "点击这里", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "来弹出窗口。", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "所提供的过期日期无效。" }, @@ -2662,14 +2588,8 @@ "dateParsingError": { "message": "保存您的删除和过期日期时出错。" }, - "hideEmail": { - "message": "对收件人隐藏我的电子邮件地址。" - }, "hideYourEmail": { - "message": "对查看者隐藏您的电子邮件地址。" - }, - "sendOptionsPolicyInEffect": { - "message": "一个或多个组织策略正在影响您的 Send 选项。" + "message": "对查看者隐藏您的电子邮箱地址。" }, "passwordPrompt": { "message": "主密码重新提示" @@ -2681,13 +2601,13 @@ "message": "此操作受到保护。若要继续,请重新输入您的主密码以验证您的身份。" }, "emailVerificationRequired": { - "message": "需要验证电子邮件" + "message": "需要验证电子邮箱" }, "emailVerifiedV2": { "message": "电子邮箱已验证" }, "emailVerificationRequiredDesc": { - "message": "您必须验证电子邮件才能使用此功能。您可以在网页密码库中验证您的电子邮件。" + "message": "您必须验证电子邮箱才能使用此功能。您可以在网页密码库中验证您的电子邮箱。" }, "updatedMasterPassword": { "message": "已更新主密码" @@ -2696,7 +2616,7 @@ "message": "更新主密码" }, "updateMasterPasswordWarning": { - "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" + "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, "updateWeakMasterPasswordWarning": { "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" @@ -2884,14 +2804,25 @@ "error": { "message": "错误" }, - "regenerateUsername": { - "message": "重新生成用户名" + "decryptionError": { + "message": "解密错误" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 无法解密下列密码库项目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "联系客户成功团队", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "以避免额外的数据丢失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "生成用户名" }, "generateEmail": { - "message": "生成电子邮件地址" + "message": "生成电子邮箱" }, "spinboxBoundariesHint": { "message": "值必须在 $MIN$ 和 $MAX$ 之间。", @@ -2908,7 +2839,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个字符生成强大的密码。", + "message": " 使用 $RECOMMENDED$ 个或更多字符生成强大的密码。", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2918,7 +2849,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个单词生成强大的密码短语。", + "message": " 使用 $RECOMMENDED$ 个或更多单词生成强大的密码短语。", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2927,18 +2858,15 @@ } } }, - "usernameType": { - "message": "用户名类型" - }, "plusAddressedEmail": { - "message": "附加地址电子邮件", + "message": "附加地址电子邮箱", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "使用您的电子邮件供应商的子地址功能。" + "message": "使用您的电子邮箱提供商的子地址功能。" }, "catchallEmail": { - "message": "Catch-all 电子邮件" + "message": "Catch-all 电子邮箱" }, "catchallEmailDesc": { "message": "使用您的域名配置的 Catch-all 收件箱。" @@ -2952,23 +2880,17 @@ "websiteName": { "message": "网站名称" }, - "whatWouldYouLikeToGenerate": { - "message": "您想要生成什么?" - }, - "passwordType": { - "message": "密码类型" - }, "service": { "message": "服务" }, "forwardedEmail": { - "message": "转发的电子邮件别名" + "message": "转发的电子邮箱别名" }, "forwardedEmailDesc": { - "message": "使用外部转发服务生成一个电子邮件别名。" + "message": "使用外部转发服务生成一个电子邮箱别名。" }, "forwarderDomainName": { - "message": "电子邮件域名", + "message": "电子邮箱域名", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -3028,7 +2950,7 @@ } }, "forwarderNoAccountId": { - "message": "无法获取 $SERVICENAME$ 电子邮件账户 ID。", + "message": "无法获取 $SERVICENAME$ 电子邮箱账户 ID。", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3157,7 +3079,7 @@ "message": "初来乍到吗?" }, "rememberEmail": { - "message": "记住电子邮件地址" + "message": "记住电子邮箱" }, "loginWithDevice": { "message": "使用设备登录" @@ -3319,7 +3241,7 @@ "message": "必须填写组织 SSO 标识符。" }, "creatingAccountOn": { - "message": "正创建账户于" + "message": "创建账户至" }, "checkYourEmail": { "message": "检查您的电子邮箱" @@ -3337,7 +3259,7 @@ "message": "返回" }, "toEditYourEmailAddress": { - "message": "编辑您的电子邮件地址。" + "message": "编辑您的电子邮箱地址。" }, "eu": { "message": "欧盟", @@ -3371,10 +3293,10 @@ "message": "登录已批准" }, "userEmailMissing": { - "message": "缺少用户电子邮件" + "message": "缺少用户电子邮箱" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "未找到活动的用户电子邮件。您将被注销。" + "message": "未找到活动的用户电子邮箱。您将被注销。" }, "deviceTrusted": { "message": "设备已信任" @@ -3442,14 +3364,14 @@ } }, "multipleInputEmails": { - "message": "一个或多个电子邮件地址无效" + "message": "一个或多个电子邮箱无效" }, "inputTrimValidator": { "message": "输入不能只包含空格。", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "输入的不是电子邮件地址。" + "message": "输入的不是电子邮箱地址。" }, "fieldsNeedAttention": { "message": "上面的 $COUNT$ 个字段需要您注意。", @@ -3503,38 +3425,6 @@ "message": "切换折叠", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "导入您的数据到 Bitwarden 吗?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "保护您的 LastPass 数据并导入到 Bitwarden 吗?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "保存为未加密的文件", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "导入到 Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "导入中...", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "数据导入成功!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "导入时出错。检查控制台以获取详细信息。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "导入过程中遇到网络错误。", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "别名域" }, @@ -4033,7 +3923,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "将 Bitwarden 设置为您的默认密码管理器吗?", + "message": "将 Bitwarden 设置为默认密码管理器吗?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { @@ -4041,7 +3931,7 @@ "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "将 Bitwarden 设置为您的默认密码管理器", + "message": "将 Bitwarden 设置为默认密码管理器", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { @@ -4088,8 +3978,11 @@ "autofillSuggestions": { "message": "自动填充建议" }, + "itemSuggestions": { + "message": "建议的项目" + }, "autofillSuggestionsTip": { - "message": "保存此站点的登录项目用来自动填充" + "message": "将此站点保存为登录项目以用于自动填充" }, "yourVaultIsEmpty": { "message": "您的密码库是空的" @@ -4098,7 +3991,7 @@ "message": "没有搜索到匹配的项目" }, "clearFiltersOrTryAnother": { - "message": "清除筛选器或尝试另一个搜索词" + "message": "清除筛选或尝试另一个搜索词" }, "copyInfoTitle": { "message": "复制信息 - $ITEMNAME$", @@ -4167,7 +4060,7 @@ "message": "分配到集合" }, "copyEmail": { - "message": "复制电子邮件地址" + "message": "复制电子邮箱" }, "copyPhone": { "message": "复制电话号码" @@ -4270,7 +4163,7 @@ "message": "所有者:您" }, "linked": { - "message": "已链接" + "message": "链接型" }, "copySuccessful": { "message": "复制成功" @@ -4498,10 +4391,10 @@ "message": "对于如密码之类的敏感数据,请使用隐藏型字段" }, "checkBoxHelpText": { - "message": "如果您想自动勾选表单复选框(例如记住电子邮件地址),请使用复选框型" + "message": "如果您想自动勾选表单复选框(例如记住电子邮箱),请使用复选框型字段" }, "linkedHelpText": { - "message": "当您处理特定网站的自动填充问题时,请使用链接型字段。" + "message": "当您处理特定网站的自动填充问题时,请使用链接型字段" }, "linkedLabelHelpText": { "message": "输入字段的 html id、名称、aria-label 或占位符。" @@ -4664,17 +4557,11 @@ "textSends": { "message": "文本 Send" }, - "bitwardenNewLook": { - "message": "Bitwarden 拥有一个新的外观!" - }, - "bitwardenNewLookDesc": { - "message": "从密码库标签页自动填充和搜索比以往任何时候都更简单直观。来看看吧!" - }, "accountActions": { "message": "账户操作" }, "showNumberOfAutofillSuggestions": { - "message": "在扩展图标上显示自动填充建议的登录的数量" + "message": "在扩展图标上显示自动填充建议的登录数量" }, "showQuickCopyActions": { "message": "在密码库上显示快速复制操作" @@ -4748,6 +4635,33 @@ "noEditPermissions": { "message": "您没有编辑此项目的权限" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "生物识别解锁不可用,因为需要先使用 PIN 或密码解锁。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物识别解锁当前不可用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "生物识别解锁不可用,因为 Bitwarden 桌面 App 已关闭。" + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "生物识别解锁不可用,因为在 Bitwarden 桌面 App 中没有为 $EMAIL$ 启用生物识别解锁。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "由于某个未知的原因,生物识别解锁当前不可用。" + }, "authenticating": { "message": "正在验证" }, @@ -4917,16 +4831,16 @@ "message": "设置两步登录" }, "newDeviceVerificationNoticeContentPage1": { - "message": "从 2025 年 02 月开始,Bitwarden 将向您的账户电子邮件地址发送一个代码,以验证从新设备上的登录。" + "message": "从 2025 年 02 月起,当有来自新设备的登录时,Bitwarden 将向您的账户电子邮箱发送验证码。" }, "newDeviceVerificationNoticeContentPage2": { - "message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮件地址更改为您可以访问的电子邮件地址。" + "message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮箱更改为您可以访问的电子邮箱。" }, "remindMeLater": { "message": "稍后提醒我" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "您可以可靠地访问您的电子邮件地址 $EMAIL$ 吗?", + "message": "您可以正常访问您的电子邮箱 $EMAIL$ 吗?", "placeholders": { "email": { "content": "$1", @@ -4938,13 +4852,13 @@ "message": "不,我不能" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "是的,我可以可靠地访问我的电子邮件地址" + "message": "是的,我可以正常访问我的电子邮箱" }, "turnOnTwoStepLogin": { "message": "开启两步登录" }, "changeAcctEmail": { - "message": "更改账户电子邮件" + "message": "更改账户电子邮箱" }, "extensionWidth": { "message": "扩展宽度" diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index ae903678521..13edf4920de 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -20,7 +20,7 @@ "message": "建立帳戶" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "第一次使用 Bitwarden?" }, "logInWithPasskey": { "message": "使用密碼金鑰登入" @@ -29,7 +29,7 @@ "message": "使用單一登入" }, "welcomeBack": { - "message": "Welcome back" + "message": "歡迎回來" }, "setAStrongPassword": { "message": "設定一個強密碼" @@ -84,7 +84,7 @@ "message": "加入組織" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "加入 $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -120,7 +120,7 @@ "message": "複製密碼" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "複製密碼短語" }, "copyNote": { "message": "複製備註" @@ -153,13 +153,13 @@ "message": "複製駕照號碼" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "複製私密金鑰" }, "copyPublicKey": { - "message": "Copy public key" + "message": "複製公開金鑰" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "複製指紋" }, "copyCustomField": { "message": "複製 $FIELD$", @@ -177,7 +177,7 @@ "message": "複製備註" }, "fill": { - "message": "Fill", + "message": "填入", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "自動填入身分資訊" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "填入驗證碼" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "填入驗證碼", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -337,22 +337,22 @@ "message": "您可以使用 Bitwarden 驗證器儲存驗證器金鑰,並為兩步驟驗證流程產生 TOTP 代碼。前往 bitwarden.com 網站以了解更多資訊。" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Bitwarden 密鑰管理" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "使用 Bitwarden 密鑰管理來安全儲存、管理並分享開發人員密鑰。在 bitwarden.com 網站上了解更多。" }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "使用 Passwordless.dev 建立流暢且安全的登入體驗,無需使用傳統密碼。在 bitwarden 網站上了解更多。" }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "免費的 Bitwarden 家庭方案" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "您符合免費 Bitwarden 家庭方案的資格要求。在網頁應用程式中兌換您的優惠。" }, "version": { "message": "版本" @@ -379,16 +379,16 @@ "message": "資料夾名稱" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "在資料夾名稱後面使用「/」來建立樹狀結構。\n例如:社交網路/論壇" }, "noFoldersAdded": { - "message": "No folders added" + "message": "未新增資料夾" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "建立資料夾來管理您的密碼庫項目" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "您確定要永久刪除此資料夾嗎?" }, "deleteFolder": { "message": "刪除資料夾" @@ -431,7 +431,7 @@ "message": "自動產生安全、唯一的登入密碼。" }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden 網頁應用程式" }, "importItems": { "message": "匯入項目" @@ -443,7 +443,7 @@ "message": "產生密碼" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "產生密碼短語" }, "regeneratePassword": { "message": "重新產生密碼" @@ -454,9 +454,6 @@ "length": { "message": "長度" }, - "passwordMinLength": { - "message": "最小密碼長度" - }, "uppercase": { "message": "大寫 (A-Z)", "description": "deprecated. Use uppercaseLabel instead." @@ -478,7 +475,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "包含大寫字元", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -486,7 +483,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "包含小寫字元", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -494,7 +491,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "包含數字", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -502,7 +499,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "包含特殊字元", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -528,16 +525,12 @@ "minSpecial": { "message": "最少符號位數" }, - "avoidAmbChar": { - "message": "避免易混淆的字元", - "description": "deprecated. Use avoidAmbiguous instead." - }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "避免易混淆的字元", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "企業原則之要求已在你的產生器選項中生效", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -571,19 +564,19 @@ "message": "我的最愛" }, "unfavorite": { - "message": "Unfavorite" + "message": "取消最愛" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "項目已加入到最愛" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "項目已從最愛中移除" }, "notes": { "message": "備註" }, "privateNote": { - "message": "Private note" + "message": "私人備註" }, "note": { "message": "備註" @@ -607,7 +600,7 @@ "message": "開啟網站" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "開啟網站 $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -658,7 +651,7 @@ "message": "您的密碼庫已鎖定。請驗證身分以繼續。" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "您的密碼庫已被鎖定" }, "yourAccountIsLocked": { "message": "您的帳戶已被鎖定。" @@ -749,7 +742,7 @@ "message": "主密碼" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "若您忘記主密碼,將會無法找回!" }, "masterPassHintLabel": { "message": "您已成功創建新帳戶!" @@ -789,7 +782,7 @@ "message": "您已成功創建新帳戶!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "你已經登入!" }, "youSuccessfullyLoggedIn": { "message": "登入成功" @@ -804,7 +797,7 @@ "message": "必須填入驗證碼。" }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "驗證已被取消或時間超過。請再試一次。" }, "invalidVerificationCode": { "message": "無效的驗證碼" @@ -841,7 +834,7 @@ "message": "Bitwarden 可以儲存並填入兩步驟驗證碼。選擇相機圖示來截取此網站的驗證器QR code,或手動複製金鑰並貼上到此欄位。" }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "了解更多驗證程式" }, "copyTOTP": { "message": "複製驗證器金鑰 (TOTP)" @@ -859,19 +852,19 @@ "message": "登入" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "登入 Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "重新啟動註冊" }, "expiredLink": { - "message": "Expired link" + "message": "過期連結" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "請重新啟動註冊流程或是重試登入。" }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "您可能已經有帳號" }, "logOutConfirmation": { "message": "您確定要登出嗎?" @@ -895,7 +888,7 @@ "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "在 Bitwarden 網頁應用程式中設定兩步驟登入,讓您的帳號更加安全。" }, "twoStepLoginConfirmationTitle": { "message": "前往網頁 App 嗎?" @@ -941,7 +934,7 @@ "message": "新增 URI" }, "addDomain": { - "message": "Add domain", + "message": "新增網域", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -1011,6 +1004,9 @@ "showIdentitiesCurrentTabDesc": { "message": "於分頁頁面顯示身分以便於自動填入。" }, + "clickToAutofillOnVault": { + "message": "在密碼庫檢視中點擊項目來自動填入" + }, "clearClipboard": { "message": "清除剪貼簿", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." @@ -1157,9 +1153,6 @@ "moveToOrganization": { "message": "移動至組織 " }, - "share": { - "message": "共用" - }, "movedItemToOrg": { "message": "已將 $ITEMNAME$ 移動至 $ORGNAME$", "placeholders": { @@ -1324,10 +1317,10 @@ "message": "輸入驗證器應用程式提供的 6 位數驗證碼。" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "驗證逾時" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "驗證工作階段因時間過久已逾時。請重試登入。" }, "enterVerificationCodeEmail": { "message": "輸入已傳送至 $EMAIL$ 的 6 位數驗證碼。", @@ -1393,17 +1386,17 @@ "message": "驗證器應用程式" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "輸入驗證器應用程式產生的驗證碼,例如 Bitwarden 驗證器。", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "YubiKey OTP 安全金鑰" }, "yubiKeyDesc": { "message": "使用 YubiKey 存取您的帳戶。支援 YubiKey 4、4 Nano、4C、以及 NEO 裝置。" }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "輸入 Duo 應用程式產生的驗證碼。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1420,7 +1413,7 @@ "message": "電子郵件" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "輸入寄送到您電子郵件信箱的驗證碼。" }, "selfHostedEnvironment": { "message": "自我裝載環境" @@ -1429,13 +1422,13 @@ "message": "指定您內部部署的 Bitwarden 安裝之基礎 URL。" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "指定您自建的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "適用於進階設定。您可以單獨指定各個服務的網域 URL。" }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "您必須新增伺服器網域 URL 或至少一個自定義環境。" }, "customEnvironment": { "message": "自訂環境" @@ -1447,7 +1440,7 @@ "message": "伺服器 URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "自建伺服器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1473,22 +1466,22 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "自動填入建議" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "在表單欄位上顯示自動填入選單" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "顯示身分建議" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "顯示信用卡建議" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "當選擇圖示時,顯示建議" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "適用於所有已登入的帳戶。" }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "關閉你的瀏覽器內建密碼管理器設定以避免衝突。" @@ -1509,7 +1502,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "頁面載入時自動填入" }, "enableAutoFillOnPageLoad": { "message": "頁面載入時自動填入" @@ -1521,7 +1514,7 @@ "message": "被入侵或不被信任的網站,可能會濫用頁面載入的自動填入功能。" }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "了解更多風險" }, "learnMoreAboutAutofill": { "message": "進一步瞭解「自動填入」功能" @@ -1551,13 +1544,13 @@ "message": "在側邊欄中開啟密碼庫" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "自動將上次使用的登入資料填入目前網站" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "自動將上次使用的信用卡填入目前網站" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "自動將上次使用的身分資料填入目前網站" }, "commandGeneratePasswordDesc": { "message": "產生一組新的隨機密碼並將它複製到剪貼簿中。" @@ -1775,7 +1768,7 @@ "message": "身分" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH 金鑰" }, "newItemHeader": { "message": "新增 $TYPE$", @@ -1808,13 +1801,13 @@ "message": "密碼歷史記錄" }, "generatorHistory": { - "message": "Generator history" + "message": "產生器歷史記錄" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "清除產生器歷史記錄" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "若繼續,所有產生器曾經產生的記錄會被刪除。您確定要繼續?" }, "back": { "message": "返回" @@ -1853,7 +1846,7 @@ "message": "安全筆記" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH 金鑰" }, "clear": { "message": "清除", @@ -1936,10 +1929,10 @@ "message": "清除歷史紀錄" }, "nothingToShow": { - "message": "Nothing to show" + "message": "沒有可顯示的內容" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "您最近未產生任何密碼" }, "remove": { "message": "移除" @@ -2009,7 +2002,7 @@ "message": "設定您用來解鎖 Bitwarden 的 PIN 碼。您的 PIN 設定將在您完全登出本應用程式時被重設。" }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "您的 PIN 碼會取代主密碼用來解鎖 Bitwarden。您的 PIN 碼會重置,若您完全登出 Bitwarden。" }, "pinRequired": { "message": "必須填入 PIN 碼。" @@ -2024,7 +2017,7 @@ "message": "使用生物特徵辨識解鎖" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "使用主密碼解鎖" }, "awaitDesktop": { "message": "等待來自桌面應用程式的確認" @@ -2036,7 +2029,7 @@ "message": "瀏覽器重啟後使用主密碼鎖定" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "瀏覽器重啟後使用主密碼鎖定" }, "selectOneCollection": { "message": "您必須至少選擇一個集合。" @@ -2047,30 +2040,27 @@ "clone": { "message": "克隆" }, - "passwordGeneratorPolicyInEffect": { - "message": "一個或多個組織原則正影響密碼產生器設定。" - }, "passwordGenerator": { - "message": "Password generator" + "message": "密碼產生器" }, "usernameGenerator": { - "message": "Username generator" + "message": "使用者名稱產生器" }, "useThisPassword": { - "message": "Use this password" + "message": "使用此密碼" }, "useThisUsername": { - "message": "Use this username" + "message": "使用此使用者名稱" }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "已產生安全的密碼!請不要忘記同時更新您網站上的密碼。" }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "使用產生器", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "來產生高強度且唯一的密碼", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { @@ -2106,7 +2096,7 @@ "message": "項目已還原" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "已經有帳號了嗎?" }, "vaultTimeoutLogOutConfirmation": { "message": "選擇登出將會在密碼庫逾時後移除對密碼庫的所有存取權限,若要重新驗證則需連線網路。確定要使用此設定嗎?" @@ -2118,7 +2108,7 @@ "message": "自動填入並儲存" }, "fillAndSave": { - "message": "Fill and save" + "message": "填入並儲存" }, "autoFillSuccessAndSavedUri": { "message": "項目已自動填入並且已儲存統一資源標識符(URI)" @@ -2199,19 +2189,19 @@ "message": "您新的主密碼不符合原則要求。" }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "獲得來自 Bitwarden 的公告、建議及研究資訊電子郵件。" }, "unsubscribe": { - "message": "Unsubscribe" + "message": "取消訂閱" }, "atAnyTime": { - "message": "at any time." + "message": "在任何時間。" }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "若是繼續,則代表您同意" }, "and": { - "message": "and" + "message": "和" }, "acceptPolicies": { "message": "選中此選取框,即表示您同意下列條款:" @@ -2232,10 +2222,10 @@ "message": "確定" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "存取權杖更新失敗" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "未找到存取權杖或 API 密鑰。請重試登出再登入。" }, "desktopSyncVerificationTitle": { "message": "桌面同步驗證" @@ -2274,10 +2264,10 @@ "message": "帳戶不相符" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "生物辨識金鑰錯誤" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "生物辨識解鎖失敗。生物辨識金鑰解鎖密碼庫失敗。請嘗試重新設定生物辨識。" }, "biometricsNotEnabledTitle": { "message": "生物特徵辨識未設定" @@ -2292,16 +2282,16 @@ "message": "此裝置不支援瀏覽器生物特徵辨識。" }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "使用者已鎖定或登出" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "請在桌面應用程式解鎖此使用者之後再重試。" }, "biometricsNotAvailableTitle": { "message": "生物辨識解鎖不可用" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "生物辨識解鎖現在無法使用。請稍後重試。" }, "biometricsFailedTitle": { "message": "生物特徵辨識失敗" @@ -2334,6 +2324,9 @@ "message": "網域", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "已封鎖的網域" + }, "excludedDomains": { "message": "排除網域" }, @@ -2343,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "對於所有已登入的帳戶,Bitwarden 不會詢問是否儲存這些網域的登入資訊。您必須重新整理頁面變更才會生效。" }, + "blockedDomainsDesc": { + "message": "自動填入及其它相關的功能無法在這些網站上使用。您必須重新整理頁面來使變更生效。" + }, + "autofillBlockedNoticeV2": { + "message": "自動填入已於此網站封鎖" + }, + "autofillBlockedNoticeGuidance": { + "message": "您可以於設定中進行更改" + }, "websiteItemLabel": { "message": "網站 $number$ (URI)", "placeholders": { @@ -2361,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "已儲存封鎖的網域" + }, "excludedDomainsSavedSuccess": { "message": "例外網域更改已儲存" }, @@ -2389,14 +2394,6 @@ "message": "Send 詳細資訊", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "searchSends": { - "message": "搜尋 Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "addSend": { - "message": "新增 Send", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendTypeText": { "message": "文字" }, @@ -2413,16 +2410,9 @@ "hideTextByDefault": { "message": "默認隱藏文字" }, - "maxAccessCountReached": { - "message": "已達最大存取次數", - "description": "This text will be displayed after a Send has been accessed the maximum amount of times." - }, "expired": { "message": "已逾期" }, - "pendingDeletion": { - "message": "等待刪除" - }, "passwordProtected": { "message": "密碼保護" }, @@ -2472,24 +2462,9 @@ "message": "編輯 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendTypeHeader": { - "message": "這是什麽類型的 Send?", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendNameDesc": { - "message": "用於描述此 Send 的易記名稱。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendFileDesc": { - "message": "您想要傳送的檔案。" - }, "deletionDate": { "message": "刪除日期" }, - "deletionDateDesc": { - "message": "此 Send 將在指定的日期和時間後被永久刪除。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "deletionDateDescV2": { "message": "此 Send 將在指定的日期後被永久刪除。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2497,10 +2472,6 @@ "expirationDate": { "message": "逾期日期" }, - "expirationDateDesc": { - "message": "如果設定此選項,對此 Send 的存取將在指定的日期和時間後逾期。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "oneDay": { "message": "1 天" }, @@ -2516,43 +2487,10 @@ "custom": { "message": "自訂" }, - "maximumAccessCount": { - "message": "最大存取次數" - }, - "maximumAccessCountDesc": { - "message": "如果設定此選項,當達到最大存取次數時,使用者將無法再次存取此 Send。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendPasswordDesc": { - "message": "選用功能。使用者需提供密碼才能存取此 Send。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, "sendPasswordDescV3": { "message": "新增一個用於收件人存取此 Send 的可選密碼。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendNotesDesc": { - "message": "關於此 Send 的私人備註。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendDisableDesc": { - "message": "停用此 Send 以阻止任何人存取。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendShareDesc": { - "message": "儲存時複製此 Send 的連結至剪貼簿。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "sendTextDesc": { - "message": "您想要傳送的文字。" - }, - "sendHideText": { - "message": "預設隱藏此 Send 的文字。", - "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." - }, - "currentAccessCount": { - "message": "目前存取次數" - }, "createSend": { "message": "建立新 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2635,18 +2573,6 @@ "sendFileCalloutHeader": { "message": "在開始之前" }, - "sendFirefoxCustomDatePopoutMessage1": { - "message": "使用行事曆樣式的日期選擇器", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage2": { - "message": "點選此處", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" - }, - "sendFirefoxCustomDatePopoutMessage3": { - "message": "要彈出至視窗。", - "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" - }, "expirationDateIsInvalid": { "message": "指定的逾期日期無效。" }, @@ -2662,15 +2588,9 @@ "dateParsingError": { "message": "儲存刪除日期和逾期日期時發生錯誤。" }, - "hideEmail": { - "message": "對收件人隱藏我的電子郵件地址。" - }, "hideYourEmail": { "message": "對查看者隱藏您的電子郵件地址。" }, - "sendOptionsPolicyInEffect": { - "message": "一個或多個組織原則正影響您的 Send 選項。" - }, "passwordPrompt": { "message": "重新詢問主密碼" }, @@ -2884,8 +2804,19 @@ "error": { "message": "錯誤" }, - "regenerateUsername": { - "message": "重新產生使用者名稱" + "decryptionError": { + "message": "解密發生錯誤" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 無法解密您密碼庫中下面的項目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "聯絡客戶支援部門", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "來避免更多資料遺失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" }, "generateUsername": { "message": "產生使用者名稱" @@ -2927,9 +2858,6 @@ } } }, - "usernameType": { - "message": "使用者名稱類型" - }, "plusAddressedEmail": { "message": "加號地址電子郵件", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" @@ -2952,12 +2880,6 @@ "websiteName": { "message": "網站名稱" }, - "whatWouldYouLikeToGenerate": { - "message": "您想要產生什麼?" - }, - "passwordType": { - "message": "密碼類型" - }, "service": { "message": "服務" }, @@ -2994,7 +2916,7 @@ "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "網站:$WEBSITE$。透過 Bitwarden 產生。", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -3004,7 +2926,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "無效的 $SERVICENAME$ API 權杖", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3014,7 +2936,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "無效的 $SERVICENAME$ API 權杖:$ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3028,7 +2950,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "無法獲得 $SERVICENAME$ 的轉送電子郵件帳號。", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3038,7 +2960,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "無效的 $SERVICENAME$ 網域。", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3048,7 +2970,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "無效的 $SERVICENAME$ URI。", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -3058,7 +2980,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "發生未知的 $SERVICENAME$ 錯誤。", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3068,7 +2990,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "未知的轉送服務提供商:$SERVICENAME$。", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3175,25 +3097,25 @@ "message": "重新傳送通知" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "notificationSentDevice": { "message": "已傳送通知至您的裝置。" }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "已傳送通知至您的裝置" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "請確保您的帳號已解鎖,並且指紋短語與其他裝置一致。" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "一旦您的請求被通過,您會獲得通知。" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "需要另一個選項嗎?" }, "loginInitiated": { "message": "登入已啟動" @@ -3253,22 +3175,22 @@ "message": "自動填入設定" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "自動填入快速鍵" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "修改鍵盤快速鍵" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "管理鍵盤快速鍵" }, "autofillShortcut": { "message": "自動填入鍵盤快速鍵" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "自動填入快速鍵尚未設定。請在瀏覽器的設定中變更。" }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "自動填入快速鍵是 $COMMAND$。請在瀏覽器的設定中變更。", "placeholders": { "command": { "content": "$1", @@ -3289,16 +3211,16 @@ "message": "在新視窗開啟" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "記住此裝置來讓未來的登入體驗更簡易" }, "deviceApprovalRequired": { "message": "裝置需要取得核准。請在下面選擇一個核准選項:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "需要核准裝置" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "選擇下面的一個核准選項" }, "rememberThisDevice": { "message": "記住這個裝置" @@ -3319,25 +3241,25 @@ "message": "需要組織 SSO 識別碼。" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "建立帳號於" }, "checkYourEmail": { - "message": "Check your email" + "message": "檢查您的電子郵件" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "跟隨電子郵件中的連結" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "並繼續建立您的帳號" }, "noEmail": { - "message": "No email?" + "message": "沒有電子郵件?" }, "goBack": { - "message": "Go back" + "message": "返回" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "來編輯您的電子郵件位址。" }, "eu": { "message": "歐盟", @@ -3374,17 +3296,17 @@ "message": "缺少使用者電子郵件地址" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "未找到使用中帳號的電子郵件。正在將您登出。" }, "deviceTrusted": { "message": "裝置已信任" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "沒有可用的 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "使用 Send 可以與任何人安全地共用加密資訊。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3461,10 +3383,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "您需注意上方的 1 個欄位。" }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "您需注意上方的 $COUNT$ 個欄位。", "placeholders": { "count": { "content": "$1", @@ -3503,38 +3425,6 @@ "message": "切換至折疊狀態", "description": "Toggling an expand/collapse state." }, - "filelessImport": { - "message": "匯入你的資料至 Bitwarden?", - "description": "Default notification title for triggering a fileless import." - }, - "lpFilelessImport": { - "message": "保護你的 LastPass 資料並匯入至 Bitwarden?", - "description": "LastPass specific notification title for triggering a fileless import." - }, - "lpCancelFilelessImport": { - "message": "儲存為未加密的檔案", - "description": "LastPass specific notification button text for cancelling a fileless import." - }, - "startFilelessImport": { - "message": "匯入至 Bitwarden", - "description": "Notification button text for starting a fileless import." - }, - "importing": { - "message": "匯入中……", - "description": "Notification message for when an import is in progress." - }, - "dataSuccessfullyImported": { - "message": "資料匯入成功!", - "description": "Notification message for when an import has completed successfully." - }, - "dataImportFailed": { - "message": "匯入時發生錯誤。檢查控制台以了解詳細資訊。", - "description": "Notification message for when an import has failed." - }, - "importNetworkError": { - "message": "匯入時遇到網路錯誤", - "description": "Notification message for when an import has failed due to a network error." - }, "aliasDomain": { "message": "別名網域" }, @@ -3551,7 +3441,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "切換側邊欄" }, "skipToContent": { "message": "跳至內容" @@ -3573,7 +3463,7 @@ "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "解鎖您的帳號來查看建議的自動填入", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -3581,15 +3471,15 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "解鎖您的帳號,並開啟在新視窗", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "基於時間的一次性驗證碼", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "現在的 TOTP 到期剩餘時間", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -3617,23 +3507,23 @@ "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "新增新的密碼庫登入項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "新信用卡", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "新增新的密碼庫信用卡項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "新身份識別", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "新增新的密碼庫身分項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -3723,19 +3613,19 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "連接到 Duo 服務時發生錯誤。使用不同的兩階段認證或聯繫 Duo 來獲得支援。" }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "啟動 Duo 並依照步驟完成登入。" }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "您的帳號要求使用 Duo 兩步驟驗證登入。" }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "彈出擴充套件視窗來完成登入。" }, "popoutExtension": { - "message": "Popout extension" + "message": "彈出擴充套件視窗" }, "launchDuo": { "message": "開啟Duo" @@ -3753,7 +3643,7 @@ "message": "檔案密碼無效,請使用您當初匯出檔案時輸入的密碼。" }, "destination": { - "message": "Destination" + "message": "目的" }, "learnAboutImportOptions": { "message": "瞭解更多匯入選項" @@ -3812,16 +3702,16 @@ "message": "確認檔案密碼" }, "exportSuccess": { - "message": "Vault data exported" + "message": "密碼庫資料已匯出" }, "typePasskey": { "message": "密碼金鑰" }, "accessing": { - "message": "Accessing" + "message": "正在存取" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "已登入!" }, "passkeyNotCopied": { "message": "密碼金鑰不會被複製" @@ -3845,7 +3735,7 @@ "message": "您沒有符合該網站的登入資訊。" }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "未找到此網站的登入資訊" }, "searchSavePasskeyNewLogin": { "message": "搜尋或將密碼金鑰儲存為新的登入資訊" @@ -3984,19 +3874,19 @@ "message": "伺服器" }, "hostedAt": { - "message": "hosted at" + "message": "架設在" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "使用您的裝置或密碼金鑰" }, "justOnce": { "message": "僅此一次" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "永遠針對此網站" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "$DOMAIN$ 已新增到排除的網域。", "placeholders": { "domain": { "content": "$1", @@ -4005,51 +3895,51 @@ } }, "commonImportFormats": { - "message": "Common formats", + "message": "常見格式", "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "繼續前往瀏覽器設定?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "繼續前往說明中心?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "在您瀏覽器的偏好設定中更改自動填入及密碼管理。", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "您可以在您瀏覽器的偏好設定中檢視及設定擴充套件的快速鍵。", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "在您瀏覽器的偏好設定中更改自動填入及密碼管理。", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "您可以在您瀏覽器的偏好設定中檢視及設定擴充套件的快速鍵。", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "使用 Bitwarden 作為預設的密碼管理器?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "忽略此設定可能會導致 Bitwarden 自動填入選單與您的瀏覽器產生衝突。", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "使用 Bitwarden 作為預設的密碼管理器", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "無法設定 Bitwarden 作為預設的密碼管理器", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "您必須同意您瀏覽器的隱私權限設定來設定 Bitwarden 為預設的密碼管理器。", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { @@ -4057,19 +3947,19 @@ "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "憑證資訊成功儲存!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "密碼已儲存!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "憑證資訊成功更新!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "密碼已更新!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -4077,7 +3967,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "成功" }, "removePasskey": { "message": "移除密碼金鑰" @@ -4086,19 +3976,22 @@ "message": "密碼金鑰已移除" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "自動填入建議" + }, + "itemSuggestions": { + "message": "建議項目" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "對此網站儲存登入項目為自動填入" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "您的密碼庫是空的" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "沒有找到相符的項目。" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "清除過濾器或更換另一個搜尋條件" }, "copyInfoTitle": { "message": "複製資訊 - $ITEMNAME$", @@ -4111,7 +4004,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "複製備註 - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4141,7 +4034,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "檢視項目 - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4151,7 +4044,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "自動填入 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4161,22 +4054,22 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "沒有資料可以複製" }, "assignToCollections": { "message": "指派至集合" }, "copyEmail": { - "message": "Copy email" + "message": "複製電子郵件地址" }, "copyPhone": { - "message": "Copy phone" + "message": "複製電話" }, "copyAddress": { - "message": "Copy address" + "message": "複製地址" }, "adminConsole": { - "message": "Admin Console" + "message": "管理控制台" }, "accountSecurity": { "message": "帳戶安全性" @@ -4188,13 +4081,13 @@ "message": "外觀" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "指定目標集合時發生錯誤。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "指定目標資料夾時發生錯誤。" }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "檢視 $NAME$ 中的項目", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4204,7 +4097,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "回到 $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4214,7 +4107,7 @@ } }, "new": { - "message": "New" + "message": "新增" }, "removeItem": { "message": "移除 $NAME$", @@ -4227,16 +4120,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "不在任何資料夾中的項目" }, "itemDetails": { "message": "項目詳細資訊" }, "itemName": { - "message": "Item name" + "message": "項目名稱" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "若您只有檢視權限,無法移除集合 $COLLECTIONS$。", "placeholders": { "collections": { "content": "$1", @@ -4245,47 +4138,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "組織已被停用" }, "owner": { - "message": "Owner" + "message": "擁有者" }, "selfOwnershipLabel": { - "message": "You", + "message": "您", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "無法存取已停用組織中的項目。請聯絡您組織的擁有者以獲取協助。" }, "additionalInformation": { - "message": "Additional information" + "message": "更多資訊" }, "itemHistory": { - "message": "Item history" + "message": "項目歷史記錄" }, "lastEdited": { - "message": "Last edited" + "message": "最後編輯" }, "ownerYou": { - "message": "Owner: You" + "message": "擁有者: 您" }, "linked": { - "message": "Linked" + "message": "連結" }, "copySuccessful": { - "message": "Copy Successful" + "message": "複製成功" }, "upload": { - "message": "Upload" + "message": "上傳" }, "addAttachment": { - "message": "Add attachment" + "message": "新增附件" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "最大檔案大小為 500MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "刪除附檔 $NAME$", "placeholders": { "name": { "content": "$1", @@ -4294,7 +4187,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "下載 $NAME$", "placeholders": { "name": { "content": "$1", @@ -4303,25 +4196,25 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "您確定要永久刪除此附檔嗎?" }, "premium": { "message": "進階版" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "免費組織無法使用附檔" }, "filters": { - "message": "Filters" + "message": "篩選器" }, "filterVault": { - "message": "Filter vault" + "message": "過濾密碼庫" }, "filterApplied": { - "message": "One filter applied" + "message": "套用了一個過濾條件" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "套用了 $COUNT$ 個過濾條件", "placeholders": { "count": { "content": "$1", @@ -4333,13 +4226,13 @@ "message": "個人資訊" }, "identification": { - "message": "Identification" + "message": "身份" }, "contactInfo": { - "message": "Contact info" + "message": "聯繫資訊" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "下載 - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4348,23 +4241,23 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "以此號碼結尾的信用卡", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "登入資訊" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "驗證器金鑰" }, "autofillOptions": { "message": "自動填入選項" }, "websiteUri": { - "message": "Website (URI)" + "message": "網站 (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "網站 (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4374,16 +4267,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "網站已新增" }, "addWebsite": { - "message": "Add website" + "message": "新增網站" }, "deleteWebsite": { - "message": "Delete website" + "message": "刪除網站" }, "defaultLabel": { - "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": { @@ -4393,7 +4286,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "顯示偵測到的吻合 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4402,7 +4295,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "隱藏偵測到的吻合 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4411,13 +4304,13 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "在頁面載入時自動填寫?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "過期的信用卡" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "如果您已續期,請更新信用卡資訊" }, "cardDetails": { "message": "信用卡詳細資料" @@ -4441,17 +4334,17 @@ "message": "新增帳戶" }, "loading": { - "message": "Loading" + "message": "正在載入" }, "data": { - "message": "Data" + "message": "資料" }, "passkeys": { "message": "密碼金鑰", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "密碼", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { @@ -4459,16 +4352,16 @@ "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "指定" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "只有可以檢視集合的組織成員才能看到其中的項目。" }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "只有可以檢視集合的組織成員才能看到其中的項目。" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "您已經選擇 $TOTAL_COUNT$ 個項目。由於您沒有編輯權限,無法更新其中的 $READONLY_COUNT$ 個項目。", "placeholders": { "total_count": { "content": "$1", @@ -4480,37 +4373,37 @@ } }, "addField": { - "message": "Add field" + "message": "新增欄位" }, "add": { - "message": "Add" + "message": "新增" }, "fieldType": { - "message": "Field type" + "message": "欄位類型" }, "fieldLabel": { - "message": "Field label" + "message": "欄位標籤" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "像是安全問答的資訊可以使用文字欄位來儲存" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "像是密碼的機密資訊可以使用隱藏欄位來儲存" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "若您想自動填入欄位中的核取方塊,例如儲存電子郵件,可以使用核取方塊。" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "使用連結欄位若您在特定網站上遇到自動填入問題。" }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "填入欄位的 html id、名稱、標籤或預留字元" }, "editField": { - "message": "Edit field" + "message": "編輯欄位" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "編輯 $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4519,7 +4412,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "刪除 $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4528,7 +4421,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ 已新增", "placeholders": { "label": { "content": "$1", @@ -4537,7 +4430,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "重新排序 $LABEL$。使用方向鍵來往上或下移動。", "placeholders": { "label": { "content": "$1", @@ -4546,7 +4439,7 @@ } }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "往上移動 $LABEL$,位置 $LENGTH$ 之 $INDEX$", "placeholders": { "label": { "content": "$1", @@ -4566,10 +4459,10 @@ "message": "選擇要指派的集合" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1 個項目會被永久移到選擇的組織。您將不再擁有此項目。" }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 個項目會被永久移到選擇的組織。您將不再擁有這些項目。", "placeholders": { "personal_items_count": { "content": "$1", @@ -4578,7 +4471,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "1 個項目會被永久移到 $ORG$。您將不再擁有此項目。", "placeholders": { "org": { "content": "$1", @@ -4587,7 +4480,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 個項目會被永久移到 $ORG$。您將不再擁有這些項目。", "placeholders": { "personal_items_count": { "content": "$1", @@ -4603,10 +4496,10 @@ "message": "指派集合成功" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "您沒有選擇任何項目。" }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "將已選取項目移動至 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4615,7 +4508,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "項目已移到 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4624,7 +4517,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "項目已移到 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4633,7 +4526,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "往下移動 $LABEL$,位置 $LENGTH$ 之 $INDEX$", "placeholders": { "label": { "content": "$1", @@ -4650,52 +4543,46 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "項目位置" }, "fileSend": { - "message": "File Send" + "message": "檔案 Send" }, "fileSends": { - "message": "File Sends" + "message": "檔案 Send" }, "textSend": { - "message": "Text Send" + "message": "文字 Send" }, "textSends": { - "message": "Text Sends" - }, - "bitwardenNewLook": { - "message": "Bitwarden has a new look!" - }, - "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + "message": "文字 Sends" }, "accountActions": { - "message": "Account actions" + "message": "帳號動作" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "在擴充套件圖示上顯示自動填入建議的數量" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "在密碼庫中顯示快速複製" }, "systemDefault": { - "message": "System default" + "message": "系統預設值" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "企業政策已套用至您的選項中" }, "sshPrivateKey": { - "message": "Private key" + "message": "私密金鑰" }, "sshPublicKey": { - "message": "Public key" + "message": "公共金鑰" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "指紋" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "金鑰類型" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4710,223 +4597,250 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "重試" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "自訂逾時時間最小為 1 分鐘。" }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "以及更多內容" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "檔案已儲存到裝置。在您的裝置上管理下載檔案。" }, "showCharacterCount": { - "message": "Show character count" + "message": "顯示字元數" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "隱藏字元數" }, "itemsInTrash": { - "message": "Items in trash" + "message": "在垃圾桶中的項目" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "垃圾桶中沒有項目" }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "您刪除的項目會在此顯示,並會在 30 天之後永久刪除" }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "垃圾桶中超過 30 天的項目將會被自動刪除。" }, "restore": { - "message": "Restore" + "message": "還原" }, "deleteForever": { - "message": "Delete forever" + "message": "永遠刪除" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "你沒有權限編輯這個項目" + }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "需要 PIN 碼或密碼解鎖才能使用生物辨識解鎖。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物辨識解鎖暫時無法使用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "由於 Bitwarden 桌面應用程式已關閉,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "由於未 Bitwarden 桌面應用程式的 $EMAIL$ 帳號上啟動,生物辨識解鎖無法使用。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "基於不明原因,生物辨識解鎖無法使用。" }, "authenticating": { - "message": "Authenticating" + "message": "驗證中" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "自動填入產生的密碼", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "密碼已重新產生", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "在 Bitwarden 中儲存登入資訊?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "空格", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "波浪", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "重音符", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "驚歎號", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "在符號", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "井字號", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "錢字號", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "百分比", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "插入號", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "和符號", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "星號", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "左括號", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "右括號", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "底線", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "連字號", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "加號", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "等號", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "左大括號", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "右大括號", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "左中括號", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "右中括號", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "垂直符號", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "反斜線", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "冒號", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "分號", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "雙引號", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "單引號", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "小於", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "大於", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "逗號", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "句號", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "問號", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "斜線", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "小寫" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "大寫" }, "generatedPassword": { - "message": "Generated password" + "message": "已產生的密碼" }, "compactMode": { - "message": "Compact mode" + "message": "緊湊模式" }, "beta": { - "message": "Beta" + "message": "測試版" }, "importantNotice": { - "message": "Important notice" + "message": "重要通知" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "啟動兩階段登入" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "從 2025 年 2 月開始,Bitwarden 會傳送代碼到您的帳號電子郵件中來驗證新裝置的登入。" }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "您可以啟動兩階段認證來保護您的帳號或更改您可以存取的電子郵件位址。" }, "remindMeLater": { - "message": "Remind me later" + "message": "稍後再提醒我" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "您可以存取您的電子郵件位址 $EMAIL$ 嗎?", "placeholders": { "email": { "content": "$1", @@ -4935,24 +4849,24 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "不,我不行" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "是,我可以存取我的電子郵件位址" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "啟動兩階段登入" }, "changeAcctEmail": { - "message": "Change account email" + "message": "更改帳號電子郵件位址" }, "extensionWidth": { - "message": "Extension width" + "message": "擴充套件寬度" }, "wide": { - "message": "Wide" + "message": "寬度" }, "extraWide": { - "message": "Extra wide" + "message": "更寬" } } diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html index f0723d75ff8..0152cd1c7ff 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html @@ -1,182 +1,78 @@ - - - - - - - - - - - - -
- -
- - - -

{{ "availableAccounts" | i18n }}

-
- -
- -
-
-
- - -

- {{ "accountLimitReached" | i18n }} -

-
+ + + + + + -
- - -

- {{ "options" | i18n }} -

-
+ + + +
+ +
- - - - - - - - - -
-
-
-
+ + +

{{ "availableAccounts" | i18n }}

+
- - -
- -
-
{{ "switchAccounts" | i18n }}
-
+
+ +
+
+
-
- -
-
-
-
-
    - -
  • - -
  • - -
    - {{ "availableAccounts" | i18n }} -
    -
  • - -
  • -
    -
    -
- -

- {{ "accountLimitReached" | i18n }} -

-
+

+ {{ "accountLimitReached" | i18n }} +

+ + -
-
{{ "options" | i18n }}
-
- - - -
-
-
-
-
+
+ + +

+ {{ "options" | i18n }} +

+
+ + + + + + + + + + +
+
+ diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index fb636ecaf6d..25e1b2ae83f 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -10,9 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserId } from "@bitwarden/common/types/guid"; import { AvatarModule, @@ -25,7 +23,6 @@ import { import { enableAccountSwitching } from "../../../platform/flags"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; -import { HeaderComponent } from "../../../platform/popup/header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -44,7 +41,6 @@ import { AccountSwitcherService } from "./services/account-switcher.service"; AvatarModule, PopupPageComponent, PopupHeaderComponent, - HeaderComponent, PopOutComponent, CurrentAccountComponent, AccountComponent, @@ -58,7 +54,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { loading = false; activeUserCanLock = false; - extensionRefreshFlag = false; enableAccountSwitching = true; constructor( @@ -70,7 +65,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private router: Router, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private authService: AuthService, - private configService: ConfigService, private lockService: LockService, ) {} @@ -109,9 +103,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { async ngOnInit() { this.enableAccountSwitching = enableAccountSwitching(); - this.extensionRefreshFlag = await this.configService.getFeatureFlag( - FeatureFlag.ExtensionRefresh, - ); const availableVaultTimeoutActions = await firstValueFrom( this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), diff --git a/apps/browser/src/auth/popup/account-switching/account.component.html b/apps/browser/src/auth/popup/account-switching/account.component.html index d062c67a2e3..d2e15d31899 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.html +++ b/apps/browser/src/auth/popup/account-switching/account.component.html @@ -1,109 +1,44 @@ - - - - - - - - - - - - + - - + + + + + diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts index 5c4132e491c..dad74977d34 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account.component.ts @@ -8,6 +8,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AvatarModule, ItemModule } from "@bitwarden/components"; +import { BiometricsService } from "@bitwarden/key-management"; import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service"; @@ -19,7 +20,6 @@ import { AccountSwitcherService, AvailableAccount } from "./services/account-swi }) export class AccountComponent { @Input() account: AvailableAccount; - @Input() extensionRefreshFlag: boolean = false; @Output() loading = new EventEmitter(); constructor( @@ -27,6 +27,7 @@ export class AccountComponent { private location: Location, private i18nService: I18nService, private logService: LogService, + private biometricsService: BiometricsService, ) {} get specialAccountAddId() { @@ -46,6 +47,9 @@ export class AccountComponent { // locked or logged out account statuses are handled by background and app.component if (result?.status === AuthenticationStatus.Unlocked) { this.location.back(); + await this.biometricsService.setShouldAutopromptNow(false); + } else { + await this.biometricsService.setShouldAutopromptNow(true); } this.loading.emit(false); } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html index 2589a08da19..8893697da17 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -20,6 +20,7 @@ [showReadonlyHostname]="showReadonlyHostname" [hideLogo]="true" [maxWidth]="maxWidth" + [hideFooter]="hideFooter" > diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 568ced70027..10ef65d0654 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -25,6 +25,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { showAcctSwitcher?: boolean; showBackButton?: boolean; showLogo?: boolean; + hideFooter?: boolean; } @Component({ @@ -54,6 +55,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { protected showReadonlyHostname: boolean; protected maxWidth: "md" | "3xl"; protected hasLoggedInAccount: boolean = false; + protected hideFooter: boolean; protected theme: string; protected logo = ExtensionBitwardenLogo; @@ -112,6 +114,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { this.pageIcon = firstChildRouteData["pageIcon"]; } + this.hideFooter = Boolean(firstChildRouteData["hideFooter"]); this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]); this.maxWidth = firstChildRouteData["maxWidth"]; @@ -158,6 +161,10 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { this.pageIcon = data.pageIcon !== null ? data.pageIcon : null; } + if (data.hideFooter !== undefined) { + this.hideFooter = data.hideFooter !== null ? data.hideFooter : null; + } + if (data.showReadonlyHostname !== undefined) { this.showReadonlyHostname = data.showReadonlyHostname; } @@ -194,6 +201,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { this.showBackButton = null; this.showLogo = null; this.maxWidth = null; + this.hideFooter = null; } ngOnDestroy() { diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index ad7e6f67361..841eefda0ad 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -25,6 +25,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, I18nMockService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; import { AccountSwitcherService } from "../account-switching/services/account-switcher.service"; diff --git a/apps/browser/src/auth/popup/home.component.html b/apps/browser/src/auth/popup/home.component.html index ed395797961..08043cf88bb 100644 --- a/apps/browser/src/auth/popup/home.component.html +++ b/apps/browser/src/auth/popup/home.component.html @@ -1,4 +1,4 @@ - +
diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 6060ac77abe..dbdcf7d5aa8 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -6,7 +6,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; 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"; @@ -30,9 +30,6 @@ export class HomeComponent implements OnInit, OnDestroy { rememberEmail: [false], }); - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - constructor( protected platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, @@ -40,7 +37,6 @@ export class HomeComponent implements OnInit, OnDestroy { private i18nService: I18nService, private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, - private registerRouteService: RegisterRouteService, private toastService: ToastService, private configService: ConfigService, private route: ActivatedRoute, @@ -118,13 +114,15 @@ export class HomeComponent implements OnInit, OnDestroy { } await this.setLoginEmailValues(); - await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); + await this.router.navigate(["login"], { + queryParams: { email: this.formGroup.controls.email.value }, + }); } async setLoginEmailValues() { // Note: Browser saves email settings here instead of the login component - this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); - await this.loginEmailService.setLoginEmail(this.formGroup.value.email); + this.loginEmailService.setRememberEmail(this.formGroup.controls.rememberEmail.value); + await this.loginEmailService.setLoginEmail(this.formGroup.controls.email.value); await this.loginEmailService.saveEmailSettings(); } } diff --git a/apps/browser/src/auth/popup/login-v1.component.ts b/apps/browser/src/auth/popup/login-v1.component.ts index cb6380d62ed..b2c52f248c6 100644 --- a/apps/browser/src/auth/popup/login-v1.component.ts +++ b/apps/browser/src/auth/popup/login-v1.component.ts @@ -10,11 +10,9 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, - RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -51,8 +49,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit { route: ActivatedRoute, loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - webAuthnLoginService: WebAuthnLoginServiceAbstraction, - registerRouteService: RegisterRouteService, toastService: ToastService, ) { super( @@ -73,8 +69,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit { route, loginEmailService, ssoLoginService, - webAuthnLoginService, - registerRouteService, toastService, ); this.onSuccessfulLogin = async () => { diff --git a/apps/browser/src/auth/popup/settings/account-security-v1.component.html b/apps/browser/src/auth/popup/settings/account-security-v1.component.html deleted file mode 100644 index dff9675743f..00000000000 --- a/apps/browser/src/auth/popup/settings/account-security-v1.component.html +++ /dev/null @@ -1,140 +0,0 @@ - -
- -
-

- {{ "accountSecurity" | i18n }} -

-
- -
-
-
-
-

{{ "unlockMethods" | i18n }}

-
-
- - -
-
- - -
-
- - -
-
-
-
-

{{ "sessionTimeoutHeader" | i18n }}

-
- - - {{ - "vaultTimeoutPolicyWithActionInEffect" - | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) - }} - - - {{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }} - - - {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} - - - - -
- - -
- -
-
-
-

{{ "otherOptions" | i18n }}

-
- - - - - -
-
-
diff --git a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts b/apps/browser/src/auth/popup/settings/account-security-v1.component.ts deleted file mode 100644 index d06724bf657..00000000000 --- a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts +++ /dev/null @@ -1,499 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { - BehaviorSubject, - combineLatest, - concatMap, - distinctUntilChanged, - filter, - firstValueFrom, - map, - Observable, - pairwise, - startWith, - Subject, - switchMap, - takeUntil, -} from "rxjs"; - -import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; -import { PinServiceAbstraction } from "@bitwarden/auth/common"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - VaultTimeout, - VaultTimeoutOption, - VaultTimeoutStringType, -} from "@bitwarden/common/types/vault-timeout.type"; -import { DialogService } from "@bitwarden/components"; -import { KeyService, BiometricStateService, BiometricsService } from "@bitwarden/key-management"; - -import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; -import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { SetPinComponent } from "../components/set-pin.component"; - -import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; - -@Component({ - selector: "auth-account-security", - templateUrl: "account-security-v1.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class AccountSecurityComponent implements OnInit, OnDestroy { - protected readonly VaultTimeoutAction = VaultTimeoutAction; - - availableVaultTimeoutActions: VaultTimeoutAction[] = []; - vaultTimeoutOptions: VaultTimeoutOption[]; - vaultTimeoutPolicyCallout: Observable<{ - timeout: { hours: string; minutes: string }; - action: VaultTimeoutAction; - }>; - supportsBiometric: boolean; - showChangeMasterPass = true; - accountSwitcherEnabled = false; - - form = this.formBuilder.group({ - vaultTimeout: [null as VaultTimeout | null], - vaultTimeoutAction: [VaultTimeoutAction.Lock], - pin: [null as boolean | null], - biometric: false, - enableAutoBiometricsPrompt: true, - }); - - private refreshTimeoutSettings$ = new BehaviorSubject(undefined); - private destroy$ = new Subject(); - - constructor( - private accountService: AccountService, - private pinService: PinServiceAbstraction, - private policyService: PolicyService, - private formBuilder: FormBuilder, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private vaultTimeoutService: VaultTimeoutService, - private vaultTimeoutSettingsService: VaultTimeoutSettingsService, - public messagingService: MessagingService, - private environmentService: EnvironmentService, - private keyService: KeyService, - private stateService: StateService, - private userVerificationService: UserVerificationService, - private dialogService: DialogService, - private changeDetectorRef: ChangeDetectorRef, - private biometricStateService: BiometricStateService, - private biometricsService: BiometricsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout); - this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe( - filter((policy) => policy != null), - map((policy) => { - let timeout; - if (policy.data?.minutes) { - timeout = { - hours: Math.floor(policy.data?.minutes / 60).toString(), - minutes: (policy.data?.minutes % 60).toString(), - }; - } - return { timeout: timeout, action: policy.data?.action }; - }), - ); - - const showOnLocked = - !this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari(); - - this.vaultTimeoutOptions = [ - { name: this.i18nService.t("immediately"), value: 0 }, - { name: this.i18nService.t("oneMinute"), value: 1 }, - { name: this.i18nService.t("fiveMinutes"), value: 5 }, - { name: this.i18nService.t("fifteenMinutes"), value: 15 }, - { name: this.i18nService.t("thirtyMinutes"), value: 30 }, - { name: this.i18nService.t("oneHour"), value: 60 }, - { name: this.i18nService.t("fourHours"), value: 240 }, - ]; - - if (showOnLocked) { - this.vaultTimeoutOptions.push({ - name: this.i18nService.t("onLocked"), - value: VaultTimeoutStringType.OnLocked, - }); - } - - this.vaultTimeoutOptions.push({ - name: this.i18nService.t("onRestart"), - value: VaultTimeoutStringType.OnRestart, - }); - this.vaultTimeoutOptions.push({ - name: this.i18nService.t("never"), - value: VaultTimeoutStringType.Never, - }); - - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - - let timeout = await firstValueFrom( - this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id), - ); - if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) { - timeout = VaultTimeoutStringType.OnRestart; - } - - const initialValues = { - vaultTimeout: timeout, - vaultTimeoutAction: await firstValueFrom( - this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), - ), - pin: await this.pinService.isPinSet(activeAccount.id), - biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(), - enableAutoBiometricsPrompt: await firstValueFrom( - this.biometricStateService.promptAutomatically$, - ), - }; - this.form.patchValue(initialValues, { emitEvent: false }); - - this.supportsBiometric = await this.biometricsService.supportsBiometric(); - this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword(); - - this.form.controls.vaultTimeout.valueChanges - .pipe( - startWith(initialValues.vaultTimeout), // emit to init pairwise - pairwise(), - concatMap(async ([previousValue, newValue]) => { - await this.saveVaultTimeout(previousValue, newValue); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.form.controls.vaultTimeoutAction.valueChanges - .pipe( - startWith(initialValues.vaultTimeoutAction), // emit to init pairwise - pairwise(), - concatMap(async ([previousValue, newValue]) => { - await this.saveVaultTimeoutAction(previousValue, newValue); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.form.controls.pin.valueChanges - .pipe( - concatMap(async (value) => { - await this.updatePin(value); - this.refreshTimeoutSettings$.next(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.form.controls.biometric.valueChanges - .pipe( - distinctUntilChanged(), - concatMap(async (enabled) => { - await this.updateBiometric(enabled); - if (enabled) { - this.form.controls.enableAutoBiometricsPrompt.enable(); - } else { - this.form.controls.enableAutoBiometricsPrompt.disable(); - } - this.refreshTimeoutSettings$.next(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.refreshTimeoutSettings$ - .pipe( - switchMap(() => - combineLatest([ - this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), - this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), - ]), - ), - takeUntil(this.destroy$), - ) - .subscribe(([availableActions, action]) => { - this.availableVaultTimeoutActions = availableActions; - this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false }); - // NOTE: The UI doesn't properly update without detect changes. - // I've even tried using an async pipe, but it still doesn't work. I'm not sure why. - // Using an async pipe means that we can't call `detectChanges` AFTER the data has change - // meaning that we are forced to use regular class variables instead of observables. - this.changeDetectorRef.detectChanges(); - }); - - this.refreshTimeoutSettings$ - .pipe( - switchMap(() => - combineLatest([ - this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), - maximumVaultTimeoutPolicy, - ]), - ), - takeUntil(this.destroy$), - ) - .subscribe(([availableActions, policy]) => { - if (policy?.data?.action || availableActions.length <= 1) { - this.form.controls.vaultTimeoutAction.disable({ emitEvent: false }); - } else { - this.form.controls.vaultTimeoutAction.enable({ emitEvent: false }); - } - }); - } - - async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) { - if (newValue === VaultTimeoutStringType.Never) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "warning" }, - content: { key: "neverLockWarning" }, - type: "warning", - }); - - if (!confirmed) { - this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false }); - return; - } - } - - // The minTimeoutError does not apply to browser because it supports Immediately - // So only check for the policyError - if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); - return; - } - - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - - const vaultTimeoutAction = await firstValueFrom( - this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id), - ); - - await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( - activeAccount.id, - newValue, - vaultTimeoutAction, - ); - if (newValue === VaultTimeoutStringType.Never) { - this.messagingService.send("bgReseedStorage"); - } - } - - async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) { - if (newValue === VaultTimeoutAction.LogOut) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "vaultTimeoutLogOutConfirmationTitle" }, - content: { key: "vaultTimeoutLogOutConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - this.form.controls.vaultTimeoutAction.setValue(previousValue, { - emitEvent: false, - }); - return; - } - } - - if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); - return; - } - - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - - await this.vaultTimeoutSettingsService.setVaultTimeoutOptions( - activeAccount.id, - this.form.value.vaultTimeout, - newValue, - ); - this.refreshTimeoutSettings$.next(); - } - - async updatePin(value: boolean) { - if (value) { - const dialogRef = SetPinComponent.open(this.dialogService); - - if (dialogRef == null) { - this.form.controls.pin.setValue(false, { emitEvent: false }); - return; - } - - const userHasPinSet = await firstValueFrom(dialogRef.closed); - this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false }); - } else { - await this.vaultTimeoutSettingsService.clear(); - } - } - - async updateBiometric(enabled: boolean) { - if (enabled && this.supportsBiometric) { - let granted; - try { - granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] }); - } catch (e) { - // eslint-disable-next-line - console.error(e); - - if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) { - await this.dialogService.openSimpleDialog({ - title: { key: "nativeMessaginPermissionSidebarTitle" }, - content: { key: "nativeMessaginPermissionSidebarDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "info", - }); - - this.form.controls.biometric.setValue(false); - return; - } - } - - if (!granted) { - await this.dialogService.openSimpleDialog({ - title: { key: "nativeMessaginPermissionErrorTitle" }, - content: { key: "nativeMessaginPermissionErrorDesc" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - - this.form.controls.biometric.setValue(false); - return; - } - - const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService); - const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed); - - await this.keyService.refreshAdditionalKeys(); - - await Promise.race([ - awaitDesktopDialogClosed.then(async (result) => { - if (result !== true) { - this.form.controls.biometric.setValue(false); - } - }), - this.biometricsService - .authenticateBiometric() - .then((result) => { - this.form.controls.biometric.setValue(result); - if (!result) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorEnableBiometricTitle"), - this.i18nService.t("errorEnableBiometricDesc"), - ); - } - }) - .catch((e) => { - // Handle connection errors - this.form.controls.biometric.setValue(false); - - const error = BiometricErrors[e.message as BiometricErrorTypes]; - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.dialogService.openSimpleDialog({ - title: { key: error.title }, - content: { key: error.description }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "danger", - }); - }) - .finally(() => { - awaitDesktopDialogRef.close(true); - }), - ]); - } else { - await this.biometricStateService.setBiometricUnlockEnabled(false); - await this.biometricStateService.setFingerprintValidated(false); - } - } - - async updateAutoBiometricsPrompt() { - await this.biometricStateService.setPromptAutomatically( - this.form.value.enableAutoBiometricsPrompt, - ); - } - - async changePassword() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "continueToWebApp" }, - content: { key: "changeMasterPasswordOnWebConfirmation" }, - type: "info", - acceptButtonText: { key: "continue" }, - }); - if (confirmed) { - const env = await firstValueFrom(this.environmentService.environment$); - await BrowserApi.createNewTab(env.getWebVaultUrl()); - } - } - - async twoStep() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "twoStepLogin" }, - content: { key: "twoStepLoginConfirmation" }, - type: "info", - }); - if (confirmed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("https://bitwarden.com/help/setup-two-step-login/"); - } - } - - async fingerprint() { - const fingerprint = await this.keyService.getFingerprint(await this.stateService.getUserId()); - - const dialogRef = FingerprintDialogComponent.open(this.dialogService, { - fingerprint, - }); - - return firstValueFrom(dialogRef.closed); - } - - async lock() { - await this.vaultTimeoutService.lock(); - } - - async logOut() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "logOut" }, - content: { key: "logOutConfirmation" }, - type: "info", - }); - - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - if (confirmed) { - this.messagingService.send("logout", { userId: userId }); - } - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } -} diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index e0dfde7be77..8bc28c9754d 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -11,13 +11,16 @@

{{ "unlockMethods" | i18n }}

- + {{ "unlockWithBiometrics" | i18n }} + + {{ biometricUnavailabilityReason }} + - + - - - {{ "lockNow" | i18n }} - - - - {{ "logOut" | i18n }} - diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index cf923ac74b5..5979afca4b8 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -17,6 +17,7 @@ import { Subject, switchMap, takeUntil, + timer, } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -28,6 +29,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -53,14 +55,17 @@ import { TypographyModule, ToastService, } from "@bitwarden/components"; -import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management"; +import { + KeyService, + BiometricsService, + BiometricStateService, + BiometricsStatus, +} from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; -import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { SetPinComponent } from "../components/set-pin.component"; @@ -82,7 +87,6 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; JslibModule, LinkModule, PopOutComponent, - PopupFooterComponent, PopupHeaderComponent, PopupPageComponent, RouterModule, @@ -101,9 +105,9 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { availableVaultTimeoutActions: VaultTimeoutAction[] = []; vaultTimeoutOptions: VaultTimeoutOption[] = []; hasVaultTimeoutPolicy = false; - supportsBiometric: boolean; + biometricUnavailabilityReason: string; showChangeMasterPass = true; - accountSwitcherEnabled = false; + showAutoPrompt = true; form = this.formBuilder.group({ vaultTimeout: [null as VaultTimeout | null], @@ -136,11 +140,14 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private biometricStateService: BiometricStateService, private toastService: ToastService, private biometricsService: BiometricsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } + ) {} async ngOnInit() { + // Firefox popup closes when unfocused by biometrics, blocking all unlock methods + if (this.platformUtilsService.getDevice() === DeviceType.FirefoxExtension) { + this.showAutoPrompt = false; + } + const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); this.showMasterPasswordOnClientRestartOption = hasMasterPassword; const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout); @@ -201,7 +208,41 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { }; this.form.patchValue(initialValues, { emitEvent: false }); - this.supportsBiometric = await this.biometricsService.supportsBiometric(); + timer(0, 1000) + .pipe( + switchMap(async () => { + const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id); + const biometricSettingAvailable = + !(await BrowserApi.permissionsGranted(["nativeMessaging"])) || + (status !== BiometricsStatus.DesktopDisconnected && + status !== BiometricsStatus.NotEnabledInConnectedDesktopApp) || + (await this.vaultTimeoutSettingsService.isBiometricLockSet()); + if (!biometricSettingAvailable) { + this.form.controls.biometric.disable({ emitEvent: false }); + } else { + this.form.controls.biometric.enable({ emitEvent: false }); + } + + if (status === BiometricsStatus.DesktopDisconnected && !biometricSettingAvailable) { + this.biometricUnavailabilityReason = this.i18nService.t( + "biometricsStatusHelptextDesktopDisconnected", + ); + } else if ( + status === BiometricsStatus.NotEnabledInConnectedDesktopApp && + !biometricSettingAvailable + ) { + this.biometricUnavailabilityReason = this.i18nService.t( + "biometricsStatusHelptextNotEnabledInDesktop", + activeAccount.email, + ); + } else { + this.biometricUnavailabilityReason = ""; + } + }), + takeUntil(this.destroy$), + ) + .subscribe(); + this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword(); this.form.controls.vaultTimeout.valueChanges @@ -401,7 +442,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { } async updateBiometric(enabled: boolean) { - if (enabled && this.supportsBiometric) { + if (enabled) { let granted; try { granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] }); @@ -473,7 +514,18 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const biometricsPromise = async () => { try { - const result = await this.biometricsService.authenticateBiometric(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a.id)), + ); + let result = false; + try { + const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId); + result = await this.keyService.validateUserKey(userKey, userId); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + result = false; + } // prevent duplicate dialog biometricsResponseReceived = true; diff --git a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts index 0917b2703cf..687dc683929 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts @@ -13,11 +13,20 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; @@ -38,7 +47,6 @@ import { ZonedMessageListenerService } from "../../platform/browser/zoned-messag AsyncActionsModule, FormsModule, ], - providers: [I18nPipe], }) export class TwoFactorAuthDuoComponent extends TwoFactorAuthDuoBaseComponent diff --git a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts index b6211bba05f..7afe1eb889e 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts @@ -6,12 +6,23 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { TwoFactorAuthEmailComponent as TwoFactorAuthEmailBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-email.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DialogService } from "../../../../../libs/components/src/dialog"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; -import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; @@ -32,7 +43,6 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; AsyncActionsModule, FormsModule, ], - providers: [I18nPipe], }) export class TwoFactorAuthEmailComponent extends TwoFactorAuthEmailBaseComponent implements OnInit { private dialogService = inject(DialogService); diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 3cb82118597..f22bbbe202c 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -37,6 +37,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index ed9d8e6d84b..1b989283112 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -31,16 +31,10 @@ interface AddUnlockVaultQueueMessage extends NotificationQueueMessage { type: "unlock"; } -interface AddRequestFilelessImportQueueMessage extends NotificationQueueMessage { - type: "fileless-import"; - importType?: string; -} - type NotificationQueueMessageItem = | AddLoginQueueMessage | AddChangePasswordQueueMessage - | AddUnlockVaultQueueMessage - | AddRequestFilelessImportQueueMessage; + | AddUnlockVaultQueueMessage; type LockedVaultPendingNotificationsData = { commandToRetry: { @@ -122,7 +116,6 @@ export { AddChangePasswordQueueMessage, AddLoginQueueMessage, AddUnlockVaultQueueMessage, - AddRequestFilelessImportQueueMessage, NotificationQueueMessageItem, LockedVaultPendingNotificationsData, AdjustNotificationBarMessageData, diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 03284f3fd89..6ad9b8e06fd 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -57,6 +57,17 @@ export type InlineMenuElementPosition = { height: number; }; +export type FieldRect = { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; + x: number; + y: number; +}; + export type InlineMenuPosition = { button?: InlineMenuElementPosition; list?: InlineMenuElementPosition; @@ -134,6 +145,7 @@ export type OverlayBackgroundExtensionMessage = { isFieldCurrentlyFilling?: boolean; subFrameData?: SubFrameOffsetData; focusedFieldData?: FocusedFieldData; + allFieldsRect?: any; isOpeningFullInlineMenu?: boolean; styles?: Partial; data?: LockedVaultPendingNotificationsData; diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts index 73f936bb591..a300ac08660 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts @@ -453,12 +453,16 @@ describe("AutoSubmitLoginBackground", () => { sendMockExtensionMessage({ command: "triggerAutoSubmitLogin" }, sender); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; }); it("skips acting on messages whose command does not have a registered handler", () => { sendMockExtensionMessage({ command: "someInvalidCommand" }, sender); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; }); diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index e043dbfdd2e..37c05a55a3a 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -60,10 +60,18 @@ describe("NotificationBackground", () => { const configService = mock(); const accountService = mock(); + const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ + id: "testId" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + beforeEach(() => { activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Locked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; + accountService.activeAccount$ = activeAccountSubject; notificationBackground = new NotificationBackground( autofillService, cipherService, @@ -683,13 +691,6 @@ describe("NotificationBackground", () => { }); describe("saveOrUpdateCredentials", () => { - const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ - id: "testId" as UserId, - email: "test@example.com", - emailVerified: true, - name: "Test User", - }); - let getDecryptedCipherByIdSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance; let updatePasswordSpy: jest.SpyInstance; diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 0947ce1e1da..0175b27bd69 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -37,7 +37,6 @@ import { AutofillService } from "../services/abstractions/autofill.service"; import { AddChangePasswordQueueMessage, AddLoginQueueMessage, - AddRequestFilelessImportQueueMessage, AddUnlockVaultQueueMessage, ChangePasswordMessageData, AddLoginMessageData, @@ -83,6 +82,8 @@ export default class NotificationBackground { getWebVaultUrlForNotification: () => this.getWebVaultUrl(), }; + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + constructor( private autofillService: AutofillService, private cipherService: CipherService, @@ -199,11 +200,6 @@ export default class NotificationBackground { case NotificationQueueMessageType.AddLogin: typeData.removeIndividualVault = await this.removeIndividualVault(); break; - case NotificationQueueMessageType.RequestFilelessImport: - typeData.importType = ( - notificationQueueMessage as AddRequestFilelessImportQueueMessage - ).importType; - break; } await BrowserApi.tabSendMessageData(tab, "openNotificationBar", { @@ -397,25 +393,6 @@ export default class NotificationBackground { } } - /** - * Sets up a notification to request a fileless import when the user - * attempts to trigger an import from a third party website. - * - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - async requestFilelessImport(tab: chrome.tabs.Tab, importType: string) { - const currentAuthStatus = await this.getAuthStatus(); - if (currentAuthStatus !== AuthenticationStatus.Unlocked || this.notificationQueue.length) { - return; - } - - const loginDomain = Utils.getDomain(tab.url); - if (loginDomain) { - await this.pushRequestFilelessImportToQueue(loginDomain, tab, importType); - } - } - private async pushChangePasswordToQueue( cipherId: string, loginDomain: string, @@ -454,36 +431,6 @@ export default class NotificationBackground { await this.sendNotificationQueueMessage(tab, message); } - /** - * Pushes a request to start a fileless import to the notification queue. - * This will display a notification bar to the user, prompting them to - * start the import. - * - * @param loginDomain - The domain of the tab that we are sending the notification to - * @param tab - The tab that we are sending the notification to - * @param importType - The type of import that is being requested - */ - private async pushRequestFilelessImportToQueue( - loginDomain: string, - tab: chrome.tabs.Tab, - importType?: string, - ) { - this.removeTabFromNotificationQueue(tab); - const launchTimestamp = new Date().getTime(); - const message: AddRequestFilelessImportQueueMessage = { - type: NotificationQueueMessageType.RequestFilelessImport, - domain: loginDomain, - tab, - launchTimestamp, - expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds - wasVaultLocked: false, - importType, - }; - this.notificationQueue.push(message); - await this.checkNotificationQueue(tab); - this.removeTabFromNotificationQueue(tab); - } - /** * Saves a cipher based on the message sent from the notification bar. If the vault * is locked, the message will be added to the notification queue and the unlock @@ -569,9 +516,7 @@ export default class NotificationBackground { return; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); const cipher = await this.cipherService.encrypt(newCipher, activeUserId); try { @@ -611,10 +556,7 @@ export default class NotificationBackground { return; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - + const activeUserId = await firstValueFrom(this.activeUserId$); const cipher = await this.cipherService.encrypt(cipherView, activeUserId); try { // We've only updated the password, no need to broadcast editedCipher message @@ -647,17 +589,15 @@ export default class NotificationBackground { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") { return false; } - - const folders = await firstValueFrom(this.folderService.folderViews$); + const activeUserId = await firstValueFrom(this.activeUserId$); + const folders = await firstValueFrom(this.folderService.folderViews$(activeUserId)); return folders.some((x) => x.id === folderId); } private async getDecryptedCipherById(cipherId: string) { const cipher = await this.cipherService.get(cipherId); if (cipher != null && cipher.type === CipherType.Login) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); return await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), @@ -697,7 +637,8 @@ export default class NotificationBackground { * Returns the first value found from the folder service's folderViews$ observable. */ private async getFolderData() { - return await firstValueFrom(this.folderService.folderViews$); + const activeUserId = await firstValueFrom(this.activeUserId$); + return await firstValueFrom(this.folderService.folderViews$(activeUserId)); } private async getWebVaultUrl(): Promise { diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index e7b72b72c9b..c3a6357ed05 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -1,5 +1,5 @@ import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -14,6 +14,7 @@ import { } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService, Region, @@ -93,6 +94,7 @@ describe("OverlayBackground", () => { let logService: MockProxy; let cipherService: MockProxy; let autofillService: MockProxy; + let configService: MockProxy; let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; let environmentMock$: BehaviorSubject; @@ -149,11 +151,13 @@ describe("OverlayBackground", () => { } beforeEach(() => { + configService = mock(); + configService.getFeatureFlag$.mockImplementation(() => of(true)); accountService = mockAccountServiceWith(mockUserId); fakeStateProvider = new FakeStateProvider(accountService); showFaviconsMock$ = new BehaviorSubject(true); neverDomainsMock$ = new BehaviorSubject({}); - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); domainSettingsService.showFavicons$ = showFaviconsMock$; domainSettingsService.neverDomains$ = neverDomainsMock$; logService = mock(); @@ -1919,7 +1923,17 @@ describe("OverlayBackground", () => { it("returns true if the overlay login ciphers are populated", async () => { overlayBackground["inlineMenuCiphers"] = new Map([ - ["inline-menu-cipher-0", mock({ type: CipherType.Login })], + [ + "inline-menu-cipher-0", + mock({ + type: CipherType.Login, + login: { + username: "username1", + password: "password1", + uri: "https://example.com", + }, + }), + ], ]); await overlayBackground["getInlineMenuCipherData"](); @@ -2899,6 +2913,124 @@ describe("OverlayBackground", () => { ); }); }); + describe("handles menu position when input is focused", () => { + it("sets button and menu width and position when non-multi-input totp field is focused", async () => { + const subframe = { + top: 0, + left: 0, + url: "", + frameId: 0, + }; + + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + focusedFieldRects: { + width: 49.328125, + height: 64, + top: 302.171875, + left: 1270.8125, + }, + }); + + const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe); + const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe); + + expect(menuPostion).toEqual({ + width: "49px", + top: "366px", + left: "1271px", + }); + expect(buttonPostion).toEqual({ + width: "34px", + height: "34px", + top: "317px", + left: "1271px", + }); + }); + it("sets button and menu width and position when multi-input totp field is focused", async () => { + const subframe = { + top: 0, + left: 0, + url: "", + frameId: 0, + }; + + const totpFields = [ + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__0" }), + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__1" }), + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__2" }), + ]; + const allFieldData = [ + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__0", + rect: { + x: 1041.5, + y: 302.171875, + width: 49.328125, + height: 64, + top: 302.171875, + right: 1090.828125, + bottom: 366.171875, + left: 1041.5, + }, + }), + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__1", + rect: { + x: 1098.828125, + y: 302.171875, + width: 49.328125, + height: 64, + top: 302.171875, + right: 1148.15625, + bottom: 366.171875, + left: 1098.828125, + }, + }), + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__2", + rect: { + x: 1156.15625, + y: 302.171875, + width: 249.328125, + height: 64, + top: 302.171875, + right: 2205.484375, + bottom: 366.171875, + left: 2156.15625, + }, + }), + ]; + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + focusedFieldRects: { + width: 49.328125, + height: 64, + top: 302.171875, + left: 1270.8125, + }, + }); + + overlayBackground["allFieldData"] = allFieldData; + jest.spyOn(overlayBackground as any, "isTotpFieldForCurrentField").mockReturnValue(true); + jest.spyOn(overlayBackground as any, "getTotpFields").mockReturnValue(totpFields); + + const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe); + const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe); + expect(menuPostion).toEqual({ + width: "1164px", + top: "366px", + left: "1042px", + }); + expect(buttonPostion).toEqual({ + width: "34px", + height: "34px", + top: "292px", + left: "2187px", + }); + }); + }); describe("triggerDelayedAutofillInlineMenuClosure message handler", () => { it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index fd16bfcf16a..3d2b1ec783c 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -66,9 +66,11 @@ import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overl import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service"; import { + areKeyValuesNull, generateDomainMatchPatterns, generateRandomChars, isInvalidResponseStatusCode, + rectHasSize, specialCharacterToKeyMap, } from "../utils"; @@ -129,6 +131,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private currentInlineMenuCiphersCount: number = 0; private currentAddNewItemData: CurrentAddNewItemData; private focusedFieldData: FocusedFieldData; + private allFieldData: AutofillField[]; private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFilling: boolean = false; private isInlineMenuButtonVisible: boolean = false; @@ -556,6 +559,28 @@ export class OverlayBackground implements OverlayBackgroundInterface { for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; + + switch (cipher.type) { + case CipherType.Card: + if (areKeyValuesNull(cipher.card)) { + continue; + } + break; + + case CipherType.Identity: + if (areKeyValuesNull(cipher.identity)) { + continue; + } + break; + + case CipherType.Login: + if ( + areKeyValuesNull(cipher.login, ["username", "password", "totp", "fido2Credentials"]) + ) { + continue; + } + break; + } if (!this.focusedFieldMatchesFillType(cipher.type)) { continue; } @@ -1344,6 +1369,71 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.isInlineMenuListVisible = false; } + /** + * Get all the totp fields for the tab and frame of the currently focused field + */ + private getTotpFields(): AutofillField[] { + const currentTabId = this.focusedFieldData?.tabId; + const currentFrameId = this.focusedFieldData?.frameId; + const pageDetailsMap = this.pageDetailsForTab[currentTabId]; + const pageDetails = pageDetailsMap?.get(currentFrameId); + + const fields = pageDetails.details.fields; + const totpFields = fields.filter((f) => + this.inlineMenuFieldQualificationService.isTotpField(f), + ); + + return totpFields; + } + + /** + * calculates the postion and width for multi-input totp field inline menu + * @param totpFieldArray - the totp fields used to evaluate the position of the menu + */ + private calculateTotpMultiInputMenuBounds(totpFieldArray: AutofillField[]) { + // Filter the fields based on the provided totpfields + const filteredObjects = this.allFieldData.filter((obj) => + totpFieldArray.some((o) => o.opid === obj.opid), + ); + + // Return null if no matching objects are found + if (filteredObjects.length === 0) { + return null; + } + // Calculate the smallest left and largest right values to determine width + const left = Math.min( + ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.left), + ); + const largestRight = Math.max( + ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.right), + ); + + const width = largestRight - left; + + return { left, width }; + } + + /** + * calculates the postion for multi-input totp field inline button + * @param totpFieldArray - the totp fields used to evaluate the position of the menu + */ + private calculateTotpMultiInputButtonBounds(totpFieldArray: AutofillField[]) { + const filteredObjects = this.allFieldData.filter((obj) => + totpFieldArray.some((o) => o.opid === obj.opid), + ); + + if (filteredObjects.length === 0) { + return null; + } + + const maxRight = Math.max(...filteredObjects.map((obj) => obj.rect.right)); + const maxObject = filteredObjects.find((obj) => obj.rect.right === maxRight); + const top = maxObject.rect.top - maxObject.rect.height * 0.39; + const left = maxRight - maxObject.rect.height * 0.3; + + return { left, top }; + } + /** * Updates the position of either the inline menu list or button. The position * is based on the focused field's position and dimensions. @@ -1449,8 +1539,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { const subFrameTopOffset = subFrameOffsets?.top || 0; const subFrameLeftOffset = subFrameOffsets?.left || 0; - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { width, height } = this.focusedFieldData.focusedFieldRects; + let { top, left } = this.focusedFieldData.focusedFieldRects; const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; + + if (this.isTotpFieldForCurrentField()) { + const totpFields = this.getTotpFields(); + if (totpFields.length > 1) { + ({ left, top } = this.calculateTotpMultiInputButtonBounds(totpFields)); + } + } + let elementOffset = height * 0.37; if (height >= 35) { elementOffset = height >= 50 ? height * 0.47 : height * 0.42; @@ -1489,7 +1588,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { const subFrameTopOffset = subFrameOffsets?.top || 0; const subFrameLeftOffset = subFrameOffsets?.left || 0; - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { top, height } = this.focusedFieldData.focusedFieldRects; + let { left, width } = this.focusedFieldData.focusedFieldRects; + + if (this.isTotpFieldForCurrentField()) { + const totpFields = this.getTotpFields(); + + if (totpFields.length > 1) { + ({ left, width } = this.calculateTotpMultiInputMenuBounds(totpFields)); + } + } this.inlineMenuPosition.list = { top: Math.round(top + height + subFrameTopOffset), @@ -1512,7 +1620,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the extension message */ private setFocusedFieldData( - { focusedFieldData }: OverlayBackgroundExtensionMessage, + { focusedFieldData, allFieldsRect }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { if ( @@ -1529,6 +1637,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { const previousFocusedFieldData = this.focusedFieldData; this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; + this.allFieldData = allFieldsRect; this.isFieldCurrentlyFocused = true; if (this.shouldUpdatePasswordGeneratorMenuOnFieldFocus()) { @@ -2275,6 +2384,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { card, identity, sender, + addNewCipherType, }: CurrentAddNewItemData) { const cipherView: CipherView = this.buildNewVaultItemCipherView({ login, @@ -2294,7 +2404,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { collectionIds: cipherView.collectionIds, }); - await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); + await this.openAddEditVaultItemPopout(sender.tab, { + cipherId: cipherView.id, + cipherType: addNewCipherType, + }); await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } catch (error) { this.logService.error("Error building cipher and opening add/edit vault item popout", error); diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index 21eadfaf668..79998b65205 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -1,12 +1,14 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -19,6 +21,7 @@ describe("context-menu", () => { let i18nService: MockProxy; let logService: MockProxy; let billingAccountProfileStateService: MockProxy; + let accountService: MockProxy; let removeAllSpy: jest.SpyInstance void]>; let createSpy: jest.SpyInstance< @@ -34,6 +37,7 @@ describe("context-menu", () => { i18nService = mock(); logService = mock(); billingAccountProfileStateService = mock(); + accountService = mock(); removeAllSpy = jest .spyOn(chrome.contextMenus, "removeAll") @@ -53,8 +57,15 @@ describe("context-menu", () => { i18nService, logService, billingAccountProfileStateService, + accountService, ); autofillSettingsService.enableContextMenu$ = of(true); + accountService.activeAccount$ = of({ + id: "userId" as UserId, + email: "", + emailVerified: false, + name: undefined, + }); }); afterEach(() => jest.resetAllMocks()); @@ -69,7 +80,7 @@ describe("context-menu", () => { }); it("has menu enabled, but does not have premium", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); const createdMenu = await sut.init(); expect(createdMenu).toBeTruthy(); @@ -77,7 +88,7 @@ describe("context-menu", () => { }); it("has menu enabled and has premium", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); const createdMenu = await sut.init(); expect(createdMenu).toBeTruthy(); @@ -131,16 +142,15 @@ describe("context-menu", () => { }); it("create entry for each cipher piece", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); await sut.loadOptions("TEST_TITLE", "1", createCipher()); - // One for autofill, copy username, copy password, and copy totp code expect(createSpy).toHaveBeenCalledTimes(4); }); it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); await sut.loadOptions("TEST_TITLE", "NOOP"); diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index e755524da47..41d88439e8f 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AUTOFILL_CARD_ID, AUTOFILL_ID, @@ -149,6 +150,7 @@ export class MainContextMenuHandler { private i18nService: I18nService, private logService: LogService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} /** @@ -168,11 +170,13 @@ export class MainContextMenuHandler { this.initRunning = true; try { + const account = await firstValueFrom(this.accountService.activeAccount$); + const hasPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ); + for (const options of this.initContextMenuItems) { - if ( - options.checkPremiumAccess && - !(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$)) - ) { + if (options.checkPremiumAccess && !hasPremium) { continue; } @@ -267,8 +271,9 @@ export class MainContextMenuHandler { await createChildItem(COPY_USERNAME_ID); } + const account = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) { await createChildItem(COPY_VERIFICATION_CODE_ID); diff --git a/apps/browser/src/autofill/content/auto-submit-login.spec.ts b/apps/browser/src/autofill/content/auto-submit-login.spec.ts index ff1dbd4e945..d70fc1e7446 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.spec.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts @@ -46,6 +46,8 @@ describe("AutoSubmitLogin content script", () => { beforeEach(() => { jest.useFakeTimers(); setupEnvironmentDefaults(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./auto-submit-login"); }); diff --git a/apps/browser/src/autofill/content/components/.lit-storybook/main.ts b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts new file mode 100644 index 00000000000..9e2da59d992 --- /dev/null +++ b/apps/browser/src/autofill/content/components/.lit-storybook/main.ts @@ -0,0 +1,67 @@ +import { dirname, join } from "path"; +import path from "path"; +import type { StorybookConfig } from "@storybook/web-components-webpack5"; +import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; +import remarkGfm from "remark-gfm"; + +const getAbsolutePath = (value: string): string => + dirname(require.resolve(join(value, "package.json"))); + +const config: StorybookConfig = { + stories: ["../lit-stories/**/*.lit-stories.@(js|jsx|ts|tsx)"], + addons: [ + getAbsolutePath("@storybook/addon-links"), + getAbsolutePath("@storybook/addon-essentials"), + getAbsolutePath("@storybook/addon-a11y"), + getAbsolutePath("@storybook/addon-designs"), + getAbsolutePath("@storybook/addon-interactions"), + { + name: "@storybook/addon-docs", + options: { + mdxPluginOptions: { + mdxCompileOptions: { + remarkPlugins: [remarkGfm], + }, + }, + }, + }, + ], + framework: { + name: getAbsolutePath("@storybook/web-components-webpack5"), + options: { + legacyRootApi: true, + }, + }, + core: { + disableTelemetry: true, + }, + env: (existingConfig) => ({ + ...existingConfig, + FLAGS: JSON.stringify({}), + }), + webpackFinal: async (config) => { + if (config.resolve) { + config.resolve.plugins = [ + new TsconfigPathsPlugin({ + configFile: path.resolve(__dirname, "../../../../../tsconfig.json"), + }), + ] as any; + } + + if (config.module && config.module.rules) { + config.module.rules.push({ + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + use: [ + { + loader: require.resolve("ts-loader"), + }, + ], + }); + } + return config; + }, + docs: {}, +}; + +export default config; diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts new file mode 100644 index 00000000000..a9b4742b448 --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -0,0 +1,66 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { border, themes, typography, spacing } from "../constants/styles"; + +export function ActionButton({ + buttonAction, + buttonText, + disabled = false, + theme, +}: { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}) { + const handleButtonClick = (event: Event) => { + if (!disabled) { + buttonAction(event); + } + }; + + return html` + + `; +} + +const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css` + ${typography.body2} + + user-select: none; + border: 1px solid transparent; + border-radius: ${border.radius.full}; + padding: ${spacing["1"]} ${spacing["3"]}; + width: 100%; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + font-weight: 700; + + ${disabled + ? ` + background-color: ${themes[theme].secondary["300"]}; + color: ${themes[theme].text.muted}; + ` + : ` + background-color: ${themes[theme].primary["600"]}; + cursor: pointer; + color: ${themes[theme].text.contrast}; + + :hover { + border-color: ${themes[theme].primary["700"]}; + background-color: ${themes[theme].primary["700"]}; + color: ${themes[theme].text.contrast}; + } + `} +`; diff --git a/apps/browser/src/autofill/content/components/buttons/badge-button.ts b/apps/browser/src/autofill/content/components/buttons/badge-button.ts new file mode 100644 index 00000000000..3b3b84f8166 --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/badge-button.ts @@ -0,0 +1,67 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { border, themes, typography, spacing } from "../constants/styles"; + +export function BadgeButton({ + buttonAction, + buttonText, + disabled = false, + theme, +}: { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}) { + const handleButtonClick = (event: Event) => { + if (!disabled) { + buttonAction(event); + } + }; + + return html` + + `; +} + +const badgeButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css` + ${typography.helperMedium} + + user-select: none; + border-radius: ${border.radius.full}; + padding: ${spacing["1"]} ${spacing["2"]}; + max-height: fit-content; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + font-weight: 500; + + ${disabled + ? ` + border: 0.5px solid ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].secondary["300"]}; + color: ${themes[theme].text.muted}; + ` + : ` + border: 0.5px solid ${themes[theme].primary["700"]}; + background-color: ${themes[theme].primary["100"]}; + cursor: pointer; + color: ${themes[theme].primary["700"]}; + + :hover { + border-color: ${themes[theme].primary["600"]}; + background-color: ${themes[theme].primary["600"]}; + color: ${themes[theme].text.contrast}; + } + `} +`; diff --git a/apps/browser/src/autofill/content/components/buttons/close-button.ts b/apps/browser/src/autofill/content/components/buttons/close-button.ts new file mode 100644 index 00000000000..c32d0c130e3 --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/close-button.ts @@ -0,0 +1,39 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { spacing, themes } from "../constants/styles"; +import { Close as CloseIcon } from "../icons"; + +export function CloseButton({ + handleCloseNotification, + theme, +}: { + handleCloseNotification: (e: Event) => void; + theme: Theme; +}) { + return html` + + `; +} + +const closeButtonStyles = (theme: Theme) => css` + border: 1px solid transparent; + border-radius: ${spacing["1"]}; + background-color: transparent; + cursor: pointer; + width: 36px; + height: 36px; + + :hover { + border: 1px solid ${themes[theme].primary["600"]}; + } + + > svg { + width: 20px; + height: 20px; + } +`; diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts new file mode 100644 index 00000000000..cacd2b59f0e --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -0,0 +1,62 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography, spacing } from "../constants/styles"; +import { PencilSquare } from "../icons"; + +export function EditButton({ + buttonAction, + buttonText, + disabled = false, + theme, +}: { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}) { + return html` + + `; +} + +const editButtonStyles = ({ disabled, theme }: { disabled?: boolean; theme: Theme }) => css` + ${typography.helperMedium} + + user-select: none; + display: flex; + border: 1px solid transparent; + border-radius: ${spacing["1"]}; + background-color: transparent; + padding: ${spacing["1"]}; + max-height: fit-content; + overflow: hidden; + + ${!disabled + ? ` + cursor: pointer; + + :hover { + border-color: ${themes[theme].primary["600"]}; + } + ` + : ""} + + > svg { + width: 16px; + height: fit-content; + } +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts new file mode 100644 index 00000000000..2d386d34d6a --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -0,0 +1,31 @@ +import { Theme } from "@bitwarden/common/platform/enums"; + +import { BadgeButton } from "../../../content/components/buttons/badge-button"; +import { EditButton } from "../../../content/components/buttons/edit-button"; +import { NotificationTypes } from "../../../notification/abstractions/notification-bar"; + +export function CipherAction({ + handleAction = () => { + /* no-op */ + }, + notificationType, + theme, +}: { + handleAction?: (e: Event) => void; + notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; + theme: Theme; +}) { + return notificationType === NotificationTypes.Change + ? BadgeButton({ + buttonAction: handleAction, + // @TODO localize + buttonText: "Update item", + theme, + }) + : EditButton({ + buttonAction: handleAction, + // @TODO localize + buttonText: "Edit item", + theme, + }); +} diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-icon.ts b/apps/browser/src/autofill/content/components/cipher/cipher-icon.ts new file mode 100644 index 00000000000..73d3f7604a9 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-icon.ts @@ -0,0 +1,33 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { Globe } from "../../../content/components/icons"; + +/** + * @param {string} props.color contextual color override if no icon URI is available + * @param {string} props.size valid CSS `width` value, represents the width-basis of the graphic, with height maintaining original aspect-ratio + */ +export function CipherIcon({ + color, + size, + theme, + uri, +}: { + color: string; + size: string; + theme: Theme; + uri?: string; +}) { + const iconClass = cipherIconStyle({ width: size }); + + return uri + ? html`` + : html`${Globe({ color, theme })}`; +} + +const cipherIconStyle = ({ width }: { width: string }) => css` + width: ${width}; + height: fit-content; +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts new file mode 100644 index 00000000000..38b4292f8e5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -0,0 +1,35 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes } from "../../../content/components/constants/styles"; +import { Business, Family } from "../../../content/components/icons"; + +// @TODO connect data source to icon checks +// @TODO support other indicator types (attachments, etc) +export function CipherInfoIndicatorIcons({ + isBusinessOrg, + isFamilyOrg, + theme, +}: { + isBusinessOrg?: boolean; + isFamilyOrg?: boolean; + theme: Theme; +}) { + const indicatorIcons = [ + ...(isBusinessOrg ? [Business({ color: themes[theme].text.muted, theme })] : []), + ...(isFamilyOrg ? [Family({ color: themes[theme].text.muted, theme })] : []), + ]; + + return indicatorIcons.length + ? html` ${indicatorIcons} ` + : null; // @TODO null case should be handled by parent +} + +const cipherInfoIndicatorIconsStyles = css` + > svg { + width: fit-content; + height: 12px; + } +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts new file mode 100644 index 00000000000..de374b44a97 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts @@ -0,0 +1,48 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography } from "../../../content/components/constants/styles"; + +import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons"; +import { CipherData } from "./types"; + +// @TODO support other cipher types (card, identity, notes, etc) +export function CipherInfo({ cipher, theme }: { cipher: CipherData; theme: Theme }) { + const { name, login } = cipher; + + return html` +
+ + ${[name, CipherInfoIndicatorIcons({ theme })]} + + + ${login?.username + ? html`${login.username}` + : null} +
+ `; +} + +const cipherInfoPrimaryTextStyles = (theme: Theme) => css` + ${typography.body2} + + gap: 2px; + display: flex; + display: block; + overflow-x: hidden; + text-overflow: ellipsis; + color: ${themes[theme].text.main}; + font-weight: 500; +`; + +const cipherInfoSecondaryTextStyles = (theme: Theme) => css` + ${typography.helperMedium} + + display: block; + overflow-x: hidden; + text-overflow: ellipsis; + color: ${themes[theme].text.muted}; + font-weight: 400; +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts new file mode 100644 index 00000000000..651c20cac3a --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts @@ -0,0 +1,65 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { spacing, themes } from "../../../content/components/constants/styles"; +import { + NotificationType, + NotificationTypes, +} from "../../../notification/abstractions/notification-bar"; + +import { CipherAction } from "./cipher-action"; +import { CipherIcon } from "./cipher-icon"; +import { CipherInfo } from "./cipher-info"; +import { CipherData } from "./types"; + +const cipherIconWidth = "24px"; + +export function CipherItem({ + cipher, + handleAction, + notificationType, + theme = ThemeTypes.Light, +}: { + cipher: CipherData; + handleAction?: (e: Event) => void; + notificationType?: NotificationType; + theme: Theme; +}) { + const { icon } = cipher; + const uri = (icon.imageEnabled && icon.image) || undefined; + + let cipherActionButton = null; + + if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) { + cipherActionButton = html`
+ ${CipherAction({ handleAction, notificationType, theme })} +
`; + } + + return html` +
+ ${CipherIcon({ color: themes[theme].text.muted, size: cipherIconWidth, theme, uri })} + ${CipherInfo({ theme, cipher })} +
+ ${cipherActionButton} + `; +} + +const cipherItemStyles = css` + gap: ${spacing["2"]}; + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: start; + + > img, + > span { + display: flex; + } + + > div { + max-width: calc(100% - ${cipherIconWidth} - ${spacing["2"]}); + } +`; diff --git a/apps/browser/src/autofill/content/components/cipher/index.ts b/apps/browser/src/autofill/content/components/cipher/index.ts new file mode 100644 index 00000000000..733ddb74b4d --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/index.ts @@ -0,0 +1,5 @@ +export * from "./cipher-action"; +export * from "./cipher-icon"; +export * from "./cipher-indicator-icons"; +export * from "./cipher-info"; +export * from "./cipher-item"; diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts new file mode 100644 index 00000000000..acdee756570 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -0,0 +1,48 @@ +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CipherTypes = { + Login: 1, + SecureNote: 2, + Card: 3, + Identity: 4, +} as const; + +type CipherType = (typeof CipherTypes)[keyof typeof CipherTypes]; + +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CipherRepromptTypes = { + None: 0, + Password: 1, +} as const; + +type CipherRepromptType = (typeof CipherRepromptTypes)[keyof typeof CipherRepromptTypes]; + +export type WebsiteIconData = { + imageEnabled: boolean; + image: string; + fallbackImage: string; + icon: string; +}; + +export type CipherData = { + id: string; + name: string; + type: CipherType; + reprompt: CipherRepromptType; + favorite: boolean; + icon: WebsiteIconData; + accountCreationFieldType?: string; + login?: { + username: string; + passkey: { + rpName: string; + userName: string; + } | null; + }; + card?: string; + identity?: { + fullName: string; + username?: string; + }; +}; diff --git a/apps/browser/src/autofill/content/components/constants/styles.ts b/apps/browser/src/autofill/content/components/constants/styles.ts new file mode 100644 index 00000000000..cd6054e90ba --- /dev/null +++ b/apps/browser/src/autofill/content/components/constants/styles.ts @@ -0,0 +1,206 @@ +import { Theme } from "@bitwarden/common/platform/enums"; + +const lightTheme = { + transparent: { + hover: `rgb(0 0 0 / 0.02)`, + }, + shadow: `rgba(168 179 200)`, + primary: { + 100: `rgba(219, 229, 246)`, + 300: `rgba(121, 161, 233)`, + 600: `rgba(23, 93, 220)`, + 700: `rgba(26, 65, 172)`, + }, + secondary: { + 100: `rgba(230, 233, 239)`, + 300: `rgba(168, 179, 200)`, + 500: `rgba(90, 109, 145)`, + 600: `rgba(83, 99, 131)`, + 700: `rgba(63, 75, 99)`, + }, + success: { + 100: `rgba(219, 229, 246)`, + 600: `rgba(121, 161, 233)`, + 700: `rgba(26, 65, 172)`, + }, + danger: { + 100: `rgba(255, 236, 239)`, + 600: `rgba(203, 38, 58)`, + 700: `rgba(149, 27, 42)`, + }, + warning: { + 100: `rgba(255, 248, 228)`, + 600: `rgba(255, 191, 0)`, + 700: `rgba(172, 88, 0)`, + }, + info: { + 100: `rgba(219, 229, 246)`, + 600: `rgba(121, 161, 233)`, + 700: `rgba(26, 65, 172)`, + }, + art: { + primary: `rgba(2, 15, 102)`, + accent: `rgba(44, 221, 223)`, + }, + text: { + main: `rgba(27, 32, 41)`, + muted: `rgba(90, 109, 145)`, + contrast: `rgba(255, 255, 255)`, + alt2: `rgba(255, 255, 255)`, + code: `rgba(192, 17, 118)`, + }, + background: { + DEFAULT: `rgba(255, 255, 255)`, + alt: `rgba(243, 246, 249)`, + alt2: `rgba(23, 92, 219)`, + alt3: `rgba(26, 65, 172)`, + alt4: `rgba(2, 15, 102)`, + }, + brandLogo: `rgba(23, 93, 220)`, +}; + +const darkTheme = { + transparent: { + hover: `rgb(255 255 255 / 0.02)`, + }, + shadow: `rgba(0, 0, 0)`, + primary: { + 100: `rgba(26, 39, 78)`, + 300: `rgba(26, 65, 172)`, + 600: `rgba(101, 171, 255)`, + 700: `rgba(170, 195, 239)`, + }, + secondary: { + 100: `rgba(48, 57, 70)`, + 300: `rgba(82, 91, 106)`, + 500: `rgba(121, 128, 142)`, + 600: `rgba(143, 152, 166)`, + 700: `rgba(158, 167, 181)`, + }, + success: { + 100: `rgba(11, 111, 21)`, + 600: `rgba(107, 241, 120)`, + 700: `rgba(191, 236, 195)`, + }, + danger: { + 100: `rgba(149, 27, 42)`, + 600: `rgba(255, 78, 99)`, + 700: `rgba(255, 236, 239)`, + }, + warning: { + 100: `rgba(172, 88, 0)`, + 600: `rgba(255, 191, 0)`, + 700: `rgba(255, 248, 228)`, + }, + info: { + 100: `rgba(26, 65, 172)`, + 600: `rgba(121, 161, 233)`, + 700: `rgba(219, 229, 246)`, + }, + art: { + primary: `rgba(243, 246, 249)`, + accent: `rgba(44, 221, 233)`, + }, + text: { + main: `rgba(243, 246, 249)`, + muted: `rgba(136, 152, 181)`, + contrast: `rgba(18 26 39)`, + alt2: `rgba(255, 255, 255)`, + code: `rgba(255, 143, 208)`, + }, + background: { + DEFAULT: `rgba(32, 39, 51)`, + alt: `rgba(18, 26, 39)`, + alt2: `rgba(47, 52, 61)`, + alt3: `rgba(48, 57, 70)`, + alt4: `rgba(18, 26, 39)`, + }, + brandLogo: `rgba(255, 255, 255)`, +}; + +export const themes = { + light: lightTheme, + dark: darkTheme, + + // For compatibility + system: lightTheme, + nord: lightTheme, + solarizedDark: darkTheme, +}; + +export const spacing = { + 4: `16px`, + 3: `12px`, + 2: `8px`, + "1.5": `6px`, + 1: `4px`, +}; + +export const border = { + radius: { + large: `8px`, + full: `9999px`, + }, +}; + +export const typography = { + body1: ` + line-height: 24px; + font-family: "DM Sans", sans-serif; + font-size: 16px; + `, + body2: ` + line-height: 20px; + font-family: "DM Sans", sans-serif; + font-size: 14px; + `, + helperMedium: ` + line-height: 16px; + font-family: "DM Sans", sans-serif; + font-size: 12px; + `, +}; + +export const ruleNames = { + fill: "fill", + stroke: "stroke", +} as const; + +type RuleName = (typeof ruleNames)[keyof typeof ruleNames]; + +/* + * `color` is an intentionally generic name here, since either fill or stroke may apply, due to + * inconsistent SVG construction. This consequently precludes dynamic multi-colored icons here. + */ +export const buildIconColorRule = (color: string, rule: RuleName = ruleNames.fill) => ` + ${rule}: ${color}; +`; + +export function scrollbarStyles(theme: Theme) { + return { + default: ` + /* FireFox & Chrome support */ + scrollbar-color: ${themes[theme].secondary["500"]} ${themes[theme].background.alt}; + `, + safari: ` + /* Safari Support */ + ::-webkit-scrollbar { + overflow: auto; + } + ::-webkit-scrollbar-thumb { + border-width: 4px; + border-style: solid; + border-radius: 0.5rem; + border-color: transparent; + background-clip: content-box; + background-color: ${themes[theme].secondary["500"]}; + } + ::-webkit-scrollbar-track { + ${themes[theme].background.alt}; + } + ::-webkit-scrollbar-thumb:hover { + ${themes[theme].secondary["600"]}; + } + `, + }; +} diff --git a/apps/browser/src/autofill/content/components/dropdown-menu.ts b/apps/browser/src/autofill/content/components/dropdown-menu.ts new file mode 100644 index 00000000000..3e3874b37d7 --- /dev/null +++ b/apps/browser/src/autofill/content/components/dropdown-menu.ts @@ -0,0 +1,121 @@ +import { css } from "@emotion/css"; +import { html, TemplateResult } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { border, themes, typography, spacing } from "./constants/styles"; +import { AngleDown } from "./icons"; + +export function DropdownMenu({ + buttonText, + icon, + disabled = false, + selectAction, + theme, +}: { + selectAction?: (e: Event) => void; + buttonText: string; + icon?: TemplateResult; + disabled?: boolean; + theme: Theme; +}) { + // @TODO placeholder/will not work; make stateful + const showDropdown = false; + const handleButtonClick = (event: Event) => { + // if (!disabled) { + // // show dropdown + // showDropdown = !showDropdown; + // this.requestUpdate(); + // } + }; + + const dropdownMenuItems: TemplateResult[] = []; + + return html` +
+ + ${showDropdown + ? html`
${dropdownMenuItems}
` + : null} +
+ `; +} + +const iconSize = "15px"; + +const dropdownContainerStyles = css` + display: flex; + + > div, + > button { + width: 100%; + } +`; + +const dropdownButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css` + ${typography.body2} + + font-weight: 400; + gap: ${spacing["1.5"]}; + user-select: none; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: space-between; + border-radius: ${border.radius.full}; + padding: ${spacing["1"]} ${spacing["2"]}; + max-height: fit-content; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + + > svg { + max-width: ${iconSize}; + height: fit-content; + } + + ${disabled + ? ` + border: 1px solid ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].secondary["300"]}; + color: ${themes[theme].text.muted}; + ` + : ` + border: 1px solid ${themes[theme].text.muted}; + background-color: transparent; + cursor: pointer; + color: ${themes[theme].text.muted}; + + :hover { + border-color: ${themes[theme].secondary["700"]}; + background-color: ${themes[theme].secondary["100"]}; + } + `} +`; + +const dropdownButtonTextStyles = css` + max-width: calc(100% - ${iconSize} - ${iconSize}); + overflow-x: hidden; + text-overflow: ellipsis; +`; + +const dropdownMenuStyles = ({ theme }: { theme: Theme }) => css` + color: ${themes[theme].text.main}; + border: 1px solid ${themes[theme].secondary["500"]}; + border-radius: 0.5rem; + background-clip: padding-box; + background-color: ${themes[theme].background.DEFAULT}; + padding: 0.25rem 0.75rem; + position: absolute; + overflow-y: auto; +`; diff --git a/apps/browser/src/autofill/content/components/icons/angle-down.ts b/apps/browser/src/autofill/content/components/icons/angle-down.ts new file mode 100644 index 00000000000..4b85319c18a --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/angle-down.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function AngleDown({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts new file mode 100644 index 00000000000..8df68d79b6e --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts @@ -0,0 +1,19 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { Shield } from "./shield"; + +export function BrandIconContainer({ iconLink, theme }: { iconLink?: URL; theme: Theme }) { + const Icon = html`
${Shield({ theme })}
`; + + return iconLink ? html`${Icon}` : Icon; +} + +const brandIconContainerStyles = css` + > svg { + width: 20px; + height: fit-content; + } +`; diff --git a/apps/browser/src/autofill/content/components/icons/business.ts b/apps/browser/src/autofill/content/components/icons/business.ts new file mode 100644 index 00000000000..547ee82b547 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/business.ts @@ -0,0 +1,46 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Business({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/close.ts b/apps/browser/src/autofill/content/components/icons/close.ts new file mode 100644 index 00000000000..c94a4b20a6a --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/close.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Close({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts new file mode 100644 index 00000000000..bcc7b3d5432 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function ExclamationTriangle({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/family.ts b/apps/browser/src/autofill/content/components/icons/family.ts new file mode 100644 index 00000000000..33e2e422ced --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/family.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Family({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/folder.ts b/apps/browser/src/autofill/content/components/icons/folder.ts new file mode 100644 index 00000000000..7e1f8f197f6 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/folder.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Folder({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/globe.ts b/apps/browser/src/autofill/content/components/icons/globe.ts new file mode 100644 index 00000000000..6697fa93b70 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/globe.ts @@ -0,0 +1,28 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Globe({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts new file mode 100644 index 00000000000..992b034afa7 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -0,0 +1,12 @@ +export { AngleDown } from "./angle-down"; +export { BrandIconContainer } from "./brand-icon-container"; +export { Business } from "./business"; +export { Close } from "./close"; +export { ExclamationTriangle } from "./exclamation-triangle"; +export { Family } from "./family"; +export { Folder } from "./folder"; +export { Globe } from "./globe"; +export { PartyHorn } from "./party-horn"; +export { PencilSquare } from "./pencil-square"; +export { Shield } from "./shield"; +export { User } from "./user"; diff --git a/apps/browser/src/autofill/content/components/icons/party-horn.ts b/apps/browser/src/autofill/content/components/icons/party-horn.ts new file mode 100644 index 00000000000..dc2144b524f --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/party-horn.ts @@ -0,0 +1,174 @@ +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +// This icon has static multi-colors for each theme +export function PartyHorn({ theme }: { theme: Theme }) { + if (theme === ThemeTypes.Dark) { + return html` + + + + + + + + + + + + + + `; + } + + return html` + + + + + + + + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/pencil-square.ts b/apps/browser/src/autofill/content/components/icons/pencil-square.ts new file mode 100644 index 00000000000..45a8429f883 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/pencil-square.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function PencilSquare({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/shield.ts b/apps/browser/src/autofill/content/components/icons/shield.ts new file mode 100644 index 00000000000..5ffd953e869 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/shield.ts @@ -0,0 +1,19 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Shield({ color, theme }: { color?: string; theme: Theme }) { + const shapeColor = color || themes[theme].brandLogo; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/user.ts b/apps/browser/src/autofill/content/components/icons/user.ts new file mode 100644 index 00000000000..6babcfa39a9 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/user.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function User({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts new file mode 100644 index 00000000000..aa53555d116 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/action-button.lit-stories.ts @@ -0,0 +1,34 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ActionButton } from "../../buttons/action-button"; + +type Args = { + buttonText: string; + disabled: boolean; + theme: Theme; + buttonAction: (e: Event) => void; +}; + +export default { + title: "Components/Buttons/Action Button", + argTypes: { + buttonText: { control: "text" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + buttonAction: { control: false }, + }, + args: { + buttonText: "Click Me", + disabled: false, + theme: ThemeTypes.Light, + buttonAction: () => alert("Clicked"), + }, +} as Meta; + +const Template = (args: Args) => ActionButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts new file mode 100644 index 00000000000..876a70eebc1 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/badge-button.lit-stories.ts @@ -0,0 +1,34 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { BadgeButton } from "../../buttons/badge-button"; + +type Args = { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}; + +export default { + title: "Components/Buttons/Badge Button", + argTypes: { + buttonText: { control: "text" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + buttonAction: { control: false }, + }, + args: { + buttonText: "Click Me", + disabled: false, + theme: ThemeTypes.Light, + buttonAction: () => alert("Clicked"), + }, +} as Meta; + +const Template = (args: Args) => BadgeButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts new file mode 100644 index 00000000000..dc202f330ae --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/close-button.lit-stories.ts @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { CloseButton } from "../../buttons/close-button"; + +type Args = { + handleCloseNotification: (e: Event) => void; + theme: Theme; +}; +export default { + title: "Components/Buttons/Close Button", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + handleCloseNotification: { control: false }, + }, + args: { + theme: ThemeTypes.Light, + handleCloseNotification: () => { + alert("Close button clicked!"); + }, + }, +} as Meta; + +const Template = (args: Args) => CloseButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts new file mode 100644 index 00000000000..769fe475dd5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/buttons/edit-button.lit-stories.ts @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { EditButton } from "../../buttons/edit-button"; + +type Args = { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}; +export default { + title: "Components/Buttons/Edit Button", + argTypes: { + buttonText: { control: "text" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + buttonAction: { control: false }, + }, + args: { + buttonText: "Click Me", + disabled: false, + theme: ThemeTypes.Light, + buttonAction: () => alert("Clicked"), + }, +} as Meta; + +const Template = (args: Args) => EditButton({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts new file mode 100644 index 00000000000..e597cddabe6 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-action.lit-stories.ts @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; +import { CipherAction } from "../../cipher/cipher-action"; + +type Args = { + handleAction?: (e: Event) => void; + notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; + theme: Theme; +}; +export default { + title: "Components/Ciphers/Cipher Action", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + notificationType: { + control: "select", + options: [NotificationTypes.Change, NotificationTypes.Add], + }, + handleAction: { control: false }, + }, + args: { + theme: ThemeTypes.Light, + notificationType: NotificationTypes.Change, + handleAction: () => { + alert("Action triggered!"); + }, + }, +} as Meta; + +const Template = (args: Args) => CipherAction({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts new file mode 100644 index 00000000000..a8884f063de --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-icon.lit-stories.ts @@ -0,0 +1,40 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { CipherIcon } from "../../cipher/cipher-icon"; + +type Args = { + color: string; + size: string; + theme: Theme; + uri?: string; +}; + +export default { + title: "Components/Ciphers/Cipher Icon", + argTypes: { + color: { control: "color" }, + size: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + uri: { control: "text" }, + }, + args: { + size: "50px", + theme: ThemeTypes.Light, + uri: "", + }, +} as Meta; + +const Template = (args: Args) => { + return html` +
+ ${CipherIcon({ ...args })} +
+ `; +}; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts new file mode 100644 index 00000000000..2d031fa3afd --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/ciphers/cipher-indicator-icon.lit-stories.ts @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { CipherInfoIndicatorIcons } from "../../cipher/cipher-indicator-icons"; + +type Args = { + isBusinessOrg?: boolean; + isFamilyOrg?: boolean; + theme: Theme; +}; + +export default { + title: "Components/Ciphers/Cipher Indicator Icon", + argTypes: { + isBusinessOrg: { control: "boolean" }, + isFamilyOrg: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + theme: ThemeTypes.Light, + isBusinessOrg: true, + isFamilyOrg: false, + }, +} as Meta; + +const Template: StoryObj["render"] = (args) => + html`
${CipherInfoIndicatorIcons({ ...args })}
`; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts new file mode 100644 index 00000000000..20c88a59246 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -0,0 +1,66 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import * as Icons from "../../icons"; + +type Args = { + color?: string; + disabled?: boolean; + theme: Theme; + size: number; + iconLink: URL; +}; + +export default { + title: "Components/Icons/Icons", + argTypes: { + iconLink: { control: "text" }, + color: { control: "color" }, + disabled: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + size: { control: "number", min: 10, max: 100, step: 1 }, + }, + args: { + iconLink: new URL("https://bitwarden.com"), + disabled: false, + theme: ThemeTypes.Light, + size: 50, + }, +} as Meta; + +const Template = (args: Args, IconComponent: (props: Args) => ReturnType) => html` +
+ ${IconComponent({ ...args })} +
+`; + +const createIconStory = (iconName: keyof typeof Icons): StoryObj => { + const story = { + render: (args) => Template(args, Icons[iconName]), + } as StoryObj; + + if (iconName !== "BrandIconContainer") { + story.argTypes = { + iconLink: { table: { disable: true } }, + }; + } + + return story; +}; + +export const AngleDownIcon = createIconStory("AngleDown"); +export const BusinessIcon = createIconStory("Business"); +export const BrandIcon = createIconStory("BrandIconContainer"); +export const CloseIcon = createIconStory("Close"); +export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); +export const FamilyIcon = createIconStory("Family"); +export const FolderIcon = createIconStory("Folder"); +export const GlobeIcon = createIconStory("Globe"); +export const PartyHornIcon = createIconStory("PartyHorn"); +export const PencilSquareIcon = createIconStory("PencilSquare"); +export const ShieldIcon = createIconStory("Shield"); +export const UserIcon = createIconStory("User"); diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts new file mode 100644 index 00000000000..00ea905a2f3 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -0,0 +1,53 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { NotificationType } from "../../../../notification/abstractions/notification-bar"; +import { CipherData } from "../../cipher/types"; +import { NotificationBody } from "../../notification/body"; + +type Args = { + ciphers: CipherData[]; + notificationType: NotificationType; + theme: Theme; +}; + +export default { + title: "Components/Notifications/Notification Body", + argTypes: { + ciphers: { control: "object" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + notificationType: { + control: "select", + options: ["add", "change", "unlock", "fileless-import"], + }, + }, + args: { + ciphers: [ + { + id: "1", + name: "Example Cipher", + type: CipherType.Login, + favorite: false, + reprompt: CipherRepromptType.None, + icon: { + imageEnabled: true, + image: "", + fallbackImage: "https://example.com/fallback.png", + icon: "icon-class", + }, + login: { username: "user@example.com", passkey: null }, + }, + ], + theme: ThemeTypes.Light, + notificationType: "change", + }, +} as Meta; + +const Template = (args: Args) => NotificationBody({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts new file mode 100644 index 00000000000..c8f30eb036f --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts @@ -0,0 +1,32 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { NotificationType } from "../../../../notification/abstractions/notification-bar"; +import { NotificationFooter } from "../../notification/footer"; + +type Args = { + notificationType: NotificationType; + theme: Theme; +}; + +export default { + title: "Components/Notifications/Notification Footer", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + notificationType: { + control: "select", + options: ["add", "change", "unlock", "fileless-import"], + }, + }, + args: { + theme: ThemeTypes.Light, + notificationType: "add", + }, +} as Meta; + +const Template = (args: Args) => NotificationFooter({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts new file mode 100644 index 00000000000..fd8423f995e --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { NotificationHeader } from "../../notification/header"; + +type Args = { + message: string; + standalone: boolean; + theme: Theme; + handleCloseNotification: (e: Event) => void; +}; + +export default { + title: "Components/Notifications/Notification Header", + argTypes: { + message: { control: "text" }, + standalone: { control: "boolean" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + message: "This is a notification message", + standalone: true, + theme: ThemeTypes.Light, + handleCloseNotification: () => alert("Close Clicked"), + }, +} as Meta; + +const Template = (args: Args) => NotificationHeader({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts new file mode 100644 index 00000000000..4b100764205 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/action-row.lit-stories.ts @@ -0,0 +1,31 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ActionRow } from "../../rows/action-row"; + +type Args = { + itemText: string; + handleAction: (e: Event) => void; + theme: Theme; +}; + +export default { + title: "Components/Rows/Action Row", + argTypes: { + itemText: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + handleAction: { control: false }, + }, + args: { + itemText: "Action Item", + theme: ThemeTypes.Light, + handleAction: () => alert("Action triggered"), + }, +} as Meta; + +const Template = (args: Args) => ActionRow({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts new file mode 100644 index 00000000000..3283c2798a3 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/button-row.lit-stories.ts @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ButtonRow } from "../../rows/button-row"; + +type Args = { + theme: Theme; +}; + +export default { + title: "Components/Rows/Button Row", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + theme: ThemeTypes.Light, + }, +} as Meta; + +const Template = (args: Args) => ButtonRow({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts new file mode 100644 index 00000000000..fbb65201986 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/rows/item-row.lit-stories.ts @@ -0,0 +1,28 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { TemplateResult } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { ItemRow } from "../../rows/item-row"; + +type Args = { + theme: Theme; + children: TemplateResult | TemplateResult[]; +}; + +export default { + title: "Components/Rows/Item Row", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + children: { control: "object" }, + }, + args: { + theme: ThemeTypes.Light, + }, +} as Meta; + +const Template = (args: Args) => ItemRow({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts new file mode 100644 index 00000000000..6a3ed2e5d1e --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -0,0 +1,69 @@ +import createEmotion from "@emotion/css/create-instance"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { NotificationType } from "../../../notification/abstractions/notification-bar"; +import { CipherItem } from "../cipher"; +import { CipherData } from "../cipher/types"; +import { scrollbarStyles, spacing, themes, typography } from "../constants/styles"; +import { ItemRow } from "../rows/item-row"; + +export const componentClassPrefix = "notification-body"; + +const { css } = createEmotion({ + key: componentClassPrefix, +}); + +export function NotificationBody({ + ciphers, + notificationType, + theme = ThemeTypes.Light, +}: { + ciphers: CipherData[]; + customClasses?: string[]; + notificationType?: NotificationType; + theme: Theme; +}) { + // @TODO get client vendor from context + const isSafari = false; + + return html` +
+ ${ciphers.map((cipher) => + ItemRow({ + theme, + children: CipherItem({ + cipher, + notificationType, + theme, + handleAction: () => { + // @TODO connect update or edit actions to handler + }, + }), + }), + )} +
+ `; +} + +const notificationBodyStyles = ({ isSafari, theme }: { isSafari: boolean; theme: Theme }) => css` + ${typography.body1} + + gap: ${spacing["1.5"]}; + display: flex; + flex-flow: column; + background-color: ${themes[theme].background.alt}; + max-height: 123px; + overflow-x: hidden; + overflow-y: auto; + white-space: nowrap; + color: ${themes[theme].text.main}; + font-weight: 400; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + } + + ${isSafari ? scrollbarStyles(theme).safari : scrollbarStyles(theme).default} +`; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts new file mode 100644 index 00000000000..8bd07ab8296 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -0,0 +1,97 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { + NotificationBarIframeInitData, + NotificationTypes, + NotificationType, +} from "../../../notification/abstractions/notification-bar"; +import { createAutofillOverlayCipherDataMock } from "../../../spec/autofill-mocks"; +import { CipherData } from "../cipher/types"; +import { themes, spacing } from "../constants/styles"; + +import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body"; +import { NotificationFooter } from "./footer"; +import { + NotificationHeader, + componentClassPrefix as notificationHeaderClassPrefix, +} from "./header"; + +export function NotificationContainer({ + handleCloseNotification, + i18n, + theme = ThemeTypes.Light, + type, +}: NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void } & { + i18n: { [key: string]: string }; + type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` +}) { + const headerMessage = getHeaderMessage(i18n, type); + const showBody = true; + + // @TODO remove mock ciphers for development + const ciphers = [ + createAutofillOverlayCipherDataMock(1), + { ...createAutofillOverlayCipherDataMock(2), icon: { imageEnabled: false } }, + { + ...createAutofillOverlayCipherDataMock(3), + icon: { imageEnabled: true, image: "https://localhost:8443/icons/webtests.dev/icon.png" }, + }, + ] as CipherData[]; + + return html` +
+ ${NotificationHeader({ + handleCloseNotification, + standalone: showBody, + message: headerMessage, + theme, + })} + ${showBody + ? NotificationBody({ + ciphers, + notificationType: type, + theme, + }) + : null} + ${NotificationFooter({ + theme, + notificationType: type, + })} +
+ `; +} + +const notificationContainerStyles = (theme: Theme) => css` + position: absolute; + right: 20px; + border: 1px solid ${themes[theme].secondary["300"]}; + border-radius: ${spacing["4"]}; + box-shadow: -2px 4px 6px 0px #0000001a; + background-color: ${themes[theme].background.alt}; + width: 400px; + + [class*="${notificationHeaderClassPrefix}-"] { + border-radius: ${spacing["4"]} ${spacing["4"]} 0 0; + } + + [class*="${notificationBodyClassPrefix}-"] { + margin: ${spacing["3"]} 0 ${spacing["1.5"]} ${spacing["3"]}; + padding-right: ${spacing["3"]}; + } +`; + +function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationType) { + switch (type) { + case NotificationTypes.Add: + return i18n.saveAsNewLoginAction; + case NotificationTypes.Change: + return i18n.updateLoginPrompt; + case NotificationTypes.Unlock: + return ""; + default: + return undefined; + } +} diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts new file mode 100644 index 00000000000..91a72dc9aab --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -0,0 +1,42 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { + NotificationType, + NotificationTypes, +} from "../../../notification/abstractions/notification-bar"; +import { spacing, themes } from "../constants/styles"; +import { ActionRow } from "../rows/action-row"; +import { ButtonRow } from "../rows/button-row"; + +export function NotificationFooter({ + notificationType, + theme, +}: { + notificationType?: NotificationType; + theme: Theme; +}) { + const isChangeNotification = notificationType === NotificationTypes.Change; + // @TODO localize + const saveNewItemText = "Save as new login"; + + return html` +
+ ${isChangeNotification + ? ActionRow({ itemText: saveNewItemText, handleAction: () => {}, theme }) + : ButtonRow({ theme })} +
+ `; +} + +const notificationFooterStyles = ({ theme }: { theme: Theme }) => css` + display: flex; + background-color: ${themes[theme].background.alt}; + padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]}; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + } +`; diff --git a/apps/browser/src/autofill/content/components/notification/header-message.ts b/apps/browser/src/autofill/content/components/notification/header-message.ts new file mode 100644 index 00000000000..ccfa58b8970 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/header-message.ts @@ -0,0 +1,25 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes } from "../constants/styles"; + +export function NotificationHeaderMessage({ message, theme }: { message: string; theme: Theme }) { + return html` + ${message} + `; +} + +const notificationHeaderMessageStyles = (theme: Theme) => css` + flex-grow: 1; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + line-height: 28px; + white-space: nowrap; + color: ${themes[theme].text.main}; + font-family: "DM Sans", sans-serif; + font-size: 18px; + font-weight: 600; +`; diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts new file mode 100644 index 00000000000..85f6e48cd5d --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/header.ts @@ -0,0 +1,61 @@ +import createEmotion from "@emotion/css/create-instance"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { CloseButton } from "../buttons/close-button"; +import { themes } from "../constants/styles"; +import { BrandIconContainer } from "../icons/brand-icon-container"; + +import { NotificationHeaderMessage } from "./header-message"; + +export const componentClassPrefix = "notification-header"; + +const { css } = createEmotion({ + key: componentClassPrefix, +}); + +export function NotificationHeader({ + message, + standalone, + theme = ThemeTypes.Light, + handleCloseNotification, +}: { + message?: string; + standalone: boolean; + theme: Theme; + handleCloseNotification: (e: Event) => void; +}) { + const showIcon = true; + const isDismissable = true; + + return html` +
+ ${showIcon ? BrandIconContainer({ theme }) : null} + ${message ? NotificationHeaderMessage({ message, theme }) : null} + ${isDismissable ? CloseButton({ handleCloseNotification, theme }) : null} +
+ `; +} + +const notificationHeaderStyles = ({ + standalone, + theme, +}: { + standalone: boolean; + theme: Theme; +}) => css` + gap: 8px; + display: flex; + align-items: center; + justify-content: flex-start; + background-color: ${themes[theme].background.alt}; + padding: 12px 16px 8px 16px; + white-space: nowrap; + + ${standalone + ? css` + border-bottom: 0.5px solid ${themes[theme].secondary["300"]}; + ` + : css``} +`; diff --git a/apps/browser/src/autofill/content/components/package.json b/apps/browser/src/autofill/content/components/package.json new file mode 100644 index 00000000000..8dbe9e7f516 --- /dev/null +++ b/apps/browser/src/autofill/content/components/package.json @@ -0,0 +1,7 @@ +{ + "name": "@bitwarden/lit-components", + "version": "2025.1.1", + "scripts": { + "storybook:lit": "storybook dev -p 6006 -c ./.lit-storybook" + } +} diff --git a/apps/browser/src/autofill/content/components/rows/action-row.ts b/apps/browser/src/autofill/content/components/rows/action-row.ts new file mode 100644 index 00000000000..ad58411baf4 --- /dev/null +++ b/apps/browser/src/autofill/content/components/rows/action-row.ts @@ -0,0 +1,53 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { spacing, themes, typography } from "../../../content/components/constants/styles"; + +export function ActionRow({ + handleAction, + itemText, + theme = ThemeTypes.Light, +}: { + itemText: string; + handleAction?: (e: Event) => void; + theme: Theme; +}) { + return html` + + `; +} + +const actionRowStyles = (theme: Theme) => css` + ${typography.body2} + + user-select: none; + border-width: 0 0 0.5px 0; + border-style: solid; + border-radius: ${spacing["2"]}; + border-color: ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].background.DEFAULT}; + cursor: pointer; + padding: ${spacing["2"]} ${spacing["3"]}; + width: 100%; + min-height: 40px; + text-align: left; + color: ${themes[theme].primary["600"]}; + font-weight: 700; + + > span { + display: block; + width: calc(100% - 5px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + :hover { + background-color: ${themes[theme].primary["100"]}; + color: ${themes[theme].primary["600"]}; + } +`; diff --git a/apps/browser/src/autofill/content/components/rows/button-row.ts b/apps/browser/src/autofill/content/components/rows/button-row.ts new file mode 100644 index 00000000000..ce14a242e97 --- /dev/null +++ b/apps/browser/src/autofill/content/components/rows/button-row.ts @@ -0,0 +1,73 @@ +import { css } from "@emotion/css"; +import { html, TemplateResult } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { ActionButton } from "../../../content/components/buttons/action-button"; +import { spacing, themes } from "../../../content/components/constants/styles"; +import { Folder, User } from "../../../content/components/icons"; +import { DropdownMenu } from "../dropdown-menu"; + +export function ButtonRow({ theme }: { theme: Theme }) { + return html` +
+ ${[ + ActionButton({ + buttonAction: () => {}, + buttonText: "Action Button", + theme, + }), + DropdownContainer({ + children: [ + DropdownMenu({ + buttonText: "You", + icon: User({ color: themes[theme].text.muted, theme }), + theme, + }), + DropdownMenu({ + buttonText: "Folder", + icon: Folder({ color: themes[theme].text.muted, theme }), + disabled: true, + theme, + }), + ], + }), + ]} +
+ `; +} + +function DropdownContainer({ children }: { children: TemplateResult[] }) { + return html`
${children}
`; +} + +const buttonRowStyles = css` + gap: 16px; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + max-height: 52px; + white-space: nowrap; + + > button { + max-width: min-content; + flex: 1 1 50%; + } + + > div { + flex: 1 1 min-content; + } +`; + +const dropdownContainerStyles = css` + gap: 8px; + display: flex; + align-items: center; + justify-content: flex-end; + overflow: hidden; + + > div { + min-width: calc(50% - ${spacing["1.5"]}); + } +`; diff --git a/apps/browser/src/autofill/content/components/rows/item-row.ts b/apps/browser/src/autofill/content/components/rows/item-row.ts new file mode 100644 index 00000000000..da00fd276ab --- /dev/null +++ b/apps/browser/src/autofill/content/components/rows/item-row.ts @@ -0,0 +1,56 @@ +import { css } from "@emotion/css"; +import { html, TemplateResult } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { spacing, themes, typography } from "../../../content/components/constants/styles"; + +export function ItemRow({ + theme = ThemeTypes.Light, + children, +}: { + theme: Theme; + children: TemplateResult | TemplateResult[]; +}) { + return html`
${children}
`; +} + +export const itemRowStyles = ({ theme }: { theme: Theme }) => css` + ${typography.body1} + + gap: ${spacing["2"]}; + display: flex; + align-items: center; + justify-content: space-between; + border-width: 0 0 0.5px 0; + border-style: solid; + border-radius: ${spacing["2"]}; + border-color: ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].background.DEFAULT}; + padding: ${spacing["2"]} ${spacing["3"]}; + min-height: min-content; + max-height: 52px; + overflow-x: hidden; + white-space: nowrap; + color: ${themes[theme].text.main}; + font-weight: 400; + + > div { + :first-child { + flex: 3 3 75%; + min-width: 25%; + } + + :not(:first-child) { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-end; + max-width: 25%; + + > button { + max-width: min-content; + } + } + } +`; diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts index 226fcb4bd61..a37a2e07678 100644 --- a/apps/browser/src/autofill/content/content-message-handler.spec.ts +++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts @@ -19,6 +19,8 @@ describe("ContentMessageHandler", () => { ); beforeEach(() => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./content-message-handler"); }); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index b260bfef632..d3e9c29ab58 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ServerConfig } from "../../../../../libs/common/src/platform/abstractions/config/server-config"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; + import { AddLoginMessageData, ChangePasswordMessageData, diff --git a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts index 1ad985bc8e9..317f63e756c 100644 --- a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts +++ b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts @@ -6,6 +6,8 @@ describe("TriggerAutofillScriptInjection", () => { describe("init", () => { it("sends a message to the extension background", () => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("../content/trigger-autofill-script-injection"); expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts index 3adaf9e276c..2c22097f3d0 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -12,6 +12,7 @@ import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService, Region, @@ -61,6 +62,7 @@ describe("OverlayBackground", () => { let overlayBackground: LegacyOverlayBackground; const cipherService = mock(); const autofillService = mock(); + let configService: MockProxy; let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; @@ -92,7 +94,9 @@ describe("OverlayBackground", () => { }; beforeEach(() => { - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + configService = mock(); + configService.getFeatureFlag$.mockImplementation(() => of(true)); + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts index fde98a58a5f..fd6a79733cb 100644 --- a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import AutofillOverlayButton from "./autofill-overlay-button.deprecated"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./legacy-button.scss"); (function () { diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts index 714ccbfbee5..5d587bd4293 100644 --- a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import AutofillOverlayList from "./autofill-overlay-list.deprecated"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./legacy-list.scss"); (function () { diff --git a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts index 1f5abac92b1..5a7b8fa990b 100644 --- a/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts +++ b/apps/browser/src/autofill/enums/notification-queue-message-type.enum.ts @@ -2,7 +2,6 @@ const NotificationQueueMessageType = { AddLogin: "add", ChangePassword: "change", UnlockVault: "unlock", - RequestFilelessImport: "fileless-import", } as const; type NotificationQueueMessageTypes = diff --git a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts index 94bef354a79..8885ed6299c 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts @@ -60,6 +60,8 @@ describe("Fido2 Content Script", () => { chrome.runtime.connect = jest.fn(() => portSpy); it("destroys the messenger when the port is disconnected", () => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); triggerPortOnDisconnectEvent(portSpy); @@ -75,6 +77,8 @@ describe("Fido2 Content Script", () => { const mockResult = { credentialId: "mock" } as CreateCredentialResult; jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); const response = await messenger.handler!(message, new AbortController()); @@ -99,6 +103,8 @@ describe("Fido2 Content Script", () => { data: mock(), }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, new AbortController()); @@ -121,6 +127,8 @@ describe("Fido2 Content Script", () => { const abortController = new AbortController(); const abortSpy = jest.spyOn(abortController.signal, "removeEventListener"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, abortController); @@ -141,6 +149,8 @@ describe("Fido2 Content Script", () => { abortController.abort(); }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, abortController); @@ -161,6 +171,8 @@ describe("Fido2 Content Script", () => { const abortController = new AbortController(); jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); const result = messenger.handler!(message, abortController); @@ -175,6 +187,8 @@ describe("Fido2 Content Script", () => { contentType: "application/json", })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); @@ -193,6 +207,8 @@ describe("Fido2 Content Script", () => { }, })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts index 6b9b41b5aac..69e17d26fe5 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts @@ -22,6 +22,8 @@ describe("FIDO2 page-script for manifest v2", () => { it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => { Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).not.toHaveBeenCalled(); @@ -33,6 +35,8 @@ describe("FIDO2 page-script for manifest v2", () => { return node; }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).toHaveBeenCalledWith("script"); @@ -48,6 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => { return node; }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).toHaveBeenCalledWith("script"); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts index fd033090cd4..4c1761c37ba 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts @@ -267,6 +267,8 @@ import { Messenger } from "./messaging/messenger"; clearWaitForFocus(); void messenger.destroy(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { /** empty */ } diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts index 31e8c941e86..f1aec69193b 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts @@ -55,6 +55,8 @@ describe("Fido2 page script with native WebAuthn support", () => { setupMockedWebAuthnSupport(); beforeAll(() => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script"); }); @@ -166,6 +168,8 @@ describe("Fido2 page script with native WebAuthn support", () => { contentType: "json/application", })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); @@ -184,6 +188,8 @@ describe("Fido2 page script with native WebAuthn support", () => { }, })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts index e354453ca59..af1838ec942 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts @@ -50,6 +50,8 @@ describe("Fido2 page script without native WebAuthn support", () => { const mockCreateCredentialsResult = createCreateCredentialResultMock(); const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); const mockCredentialAssertResult = createAssertCredentialResultMock(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script"); afterEach(() => { diff --git a/apps/browser/src/autofill/models/autofill-field.ts b/apps/browser/src/autofill/models/autofill-field.ts index 7660b4ce5f0..c0be60f1cd0 100644 --- a/apps/browser/src/autofill/models/autofill-field.ts +++ b/apps/browser/src/autofill/models/autofill-field.ts @@ -1,3 +1,4 @@ +import { FieldRect } from "../background/abstractions/overlay.background"; // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { AutofillFieldQualifierType } from "../enums/autofill-field.enums"; @@ -124,4 +125,9 @@ export default class AutofillField { fieldQualifier?: AutofillFieldQualifierType; accountCreationFieldType?: InlineMenuAccountCreationFieldTypes; + + /** + * used for totp multiline calculations + */ + fieldRect?: FieldRect; } diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 425d53783e1..53948a26a2e 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -1,7 +1,15 @@ import { Theme } from "@bitwarden/common/platform/enums"; +const NotificationTypes = { + Add: "add", + Change: "change", + Unlock: "unlock", +} as const; + +type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; + type NotificationBarIframeInitData = { - type?: string; + type?: string; // @TODO use `NotificationType` isVaultLocked?: boolean; theme?: Theme; removeIndividualVault?: boolean; @@ -24,6 +32,8 @@ type NotificationBarWindowMessageHandlers = { }; export { + NotificationTypes, + NotificationType, NotificationBarIframeInitData, NotificationBarWindowMessage, NotificationBarWindowMessageHandlers, diff --git a/apps/browser/src/autofill/notification/bar.html b/apps/browser/src/autofill/notification/bar.html index 6b0e76b5169..b686e1ec2f5 100644 --- a/apps/browser/src/autofill/notification/bar.html +++ b/apps/browser/src/autofill/notification/bar.html @@ -55,14 +55,4 @@ - - diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index a4b8ae44b6a..3fc61c448fe 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -4,7 +4,6 @@ import { ThemeTypes } from "@bitwarden/common/platform/enums"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { FilelessImportPort, FilelessImportType } from "../../tools/enums/fileless-import.enums"; import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -15,6 +14,8 @@ import { NotificationBarIframeInitData, } from "./abstractions/notification-bar"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./bar.scss"); const logService = new ConsoleLogService(false); @@ -57,11 +58,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { notificationChangeDesc: chrome.i18n.getMessage("notificationChangeDesc"), notificationUnlock: chrome.i18n.getMessage("notificationUnlock"), notificationUnlockDesc: chrome.i18n.getMessage("notificationUnlockDesc"), - filelessImport: chrome.i18n.getMessage("filelessImport"), - lpFilelessImport: chrome.i18n.getMessage("lpFilelessImport"), - cancelFilelessImport: chrome.i18n.getMessage("no"), - lpCancelFilelessImport: chrome.i18n.getMessage("lpCancelFilelessImport"), - startFilelessImport: chrome.i18n.getMessage("startFilelessImport"), }; setupLogoLink(i18n); @@ -105,22 +101,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc; - // i18n for "Fileless Import" (fileless-import) template - const isLpImport = initData.importType === FilelessImportType.LP; - const importTemplate = document.getElementById("template-fileless-import") as HTMLTemplateElement; - - const startImportButton = importTemplate.content.getElementById("start-fileless-import"); - startImportButton.textContent = i18n.startFilelessImport; - - const cancelImportButton = importTemplate.content.getElementById("cancel-fileless-import"); - cancelImportButton.textContent = isLpImport - ? i18n.lpCancelFilelessImport - : i18n.cancelFilelessImport; - - importTemplate.content.getElementById("fileless-import-text").textContent = isLpImport - ? i18n.lpFilelessImport - : i18n.filelessImport; - // i18n for body content const closeButton = document.getElementById("close-button"); closeButton.title = i18n.close; @@ -132,8 +112,6 @@ function initNotificationBar(message: NotificationBarWindowMessage) { handleTypeChange(); } else if (notificationType === "unlock") { handleTypeUnlock(); - } else if (notificationType === "fileless-import") { - handleTypeFilelessImport(); } closeButton.addEventListener("click", (e) => { @@ -247,59 +225,6 @@ function handleTypeUnlock() { }); } -/** - * Sets up a port to communicate with the fileless importer content script. - * This connection to the background script is used to trigger the action of - * downloading the CSV file from the LP importer or importing the data into - * the Bitwarden vault. - */ -function handleTypeFilelessImport() { - const importType = notificationBarIframeInitData.importType; - const port = chrome.runtime.connect({ name: FilelessImportPort.NotificationBar }); - setContent(document.getElementById("template-fileless-import") as HTMLTemplateElement); - - const startFilelessImportButton = document.getElementById("start-fileless-import"); - const startFilelessImport = () => { - port.postMessage({ command: "startFilelessImport", importType }); - document.getElementById("fileless-import-buttons").textContent = - chrome.i18n.getMessage("importing"); - startFilelessImportButton.removeEventListener("click", startFilelessImport); - }; - startFilelessImportButton.addEventListener("click", startFilelessImport); - - const cancelFilelessImportButton = document.getElementById("cancel-fileless-import"); - cancelFilelessImportButton.addEventListener("click", () => { - port.postMessage({ command: "cancelFilelessImport", importType }); - }); - - const handlePortMessage = (msg: any) => { - if (msg.command !== "filelessImportCompleted" && msg.command !== "filelessImportFailed") { - return; - } - - port.disconnect(); - - const filelessImportButtons = document.getElementById("fileless-import-buttons"); - const notificationBarOuterWrapper = document.getElementById("notification-bar-outer-wrapper"); - - if (msg.command === "filelessImportCompleted") { - filelessImportButtons.textContent = chrome.i18n.getMessage("dataSuccessfullyImported"); - filelessImportButtons.prepend(buildSvgDomElement(circleCheckIcon)); - filelessImportButtons.classList.add("success-message"); - notificationBarOuterWrapper.classList.add("success-event"); - adjustHeight(); - return; - } - - filelessImportButtons.textContent = chrome.i18n.getMessage("dataImportFailed"); - filelessImportButtons.classList.add("error-message"); - notificationBarOuterWrapper.classList.add("error-event"); - adjustHeight(); - logService.error(`Error Encountered During Import: ${msg.importErrorMessage}`); - }; - port.onMessage.addListener(handlePortMessage); -} - function setContent(template: HTMLTemplateElement) { const content = document.getElementById("content"); while (content.firstChild) { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts index 0ed14a520c1..36ef3897c56 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import { AutofillInlineMenuButton } from "./autofill-inline-menu-button"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./button.scss"); (function () { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap index 3b8458ec2ab..acd06fb8c65 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/__snapshots__/autofill-inline-menu-list.spec.ts.snap @@ -681,6 +681,7 @@ exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers f class="cipher-container" > - - - -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - - -
    -
  • -
  • -
    - -
    -
  • -
  • -
    - - -
    - diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts deleted file mode 100644 index 2101979fe36..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; - -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - -@Component({ - selector: "app-fido2-cipher-row-v1", - templateUrl: "fido2-cipher-row-v1.component.html", - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Fido2CipherRowV1Component { - @Output() onSelected = new EventEmitter(); - @Input() cipher: CipherView; - @Input() last: boolean; - @Input() title: string; - @Input() isSearching: boolean; - @Input() isSelected: boolean; - - protected selectCipher(c: CipherView) { - this.onSelected.emit(c); - } - - /** - * Returns a subname for the cipher. - * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId. - * @param c Cipher - * @returns - */ - protected getSubName(c: CipherView): string | null { - const fido2Credentials = c.login?.fido2Credentials; - - if (!fido2Credentials || fido2Credentials.length === 0) { - return null; - } - - const [fido2Credential] = fido2Credentials; - - return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null; - } -} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html deleted file mode 100644 index 9f6c0aca50d..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - -
    - -
    -
    - -
    -
    diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts deleted file mode 100644 index b7a1bc195ad..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts +++ /dev/null @@ -1,115 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; -import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; - -@Component({ - selector: "app-fido2-use-browser-link-v1", - templateUrl: "fido2-use-browser-link-v1.component.html", - animations: [ - trigger("transformPanel", [ - state( - "void", - style({ - opacity: 0, - }), - ), - transition( - "void => open", - animate( - "100ms linear", - style({ - opacity: 1, - }), - ), - ), - transition("* => void", animate("100ms linear", style({ opacity: 0 }))), - ]), - ], -}) -export class Fido2UseBrowserLinkV1Component { - showOverlay = false; - isOpen = false; - overlayPosition: ConnectedPosition[] = [ - { - originX: "start", - originY: "bottom", - overlayX: "start", - overlayY: "top", - offsetY: 5, - }, - ]; - - protected fido2PopoutSessionData$ = fido2PopoutSessionData$(); - - constructor( - private domainSettingsService: DomainSettingsService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - ) {} - - toggle() { - this.isOpen = !this.isOpen; - } - - close() { - this.isOpen = false; - } - - /** - * Aborts the current FIDO2 session and fallsback to the browser. - * @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts. - */ - protected async abort(excludeDomain = true) { - this.close(); - const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); - - if (!excludeDomain) { - this.abortSession(sessionData.sessionId); - return; - } - // Show overlay to prevent the user from interacting with the page. - this.showOverlay = true; - await this.handleDomainExclusion(sessionData.senderUrl); - // Give the user a chance to see the toast before closing the popout. - await Utils.delay(2000); - this.abortSession(sessionData.sessionId); - } - - /** - * Excludes the domain from future FIDO2 prompts. - * @param uri - The domain uri to exclude from future FIDO2 prompts. - */ - private async handleDomainExclusion(uri: string) { - const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); - - const validDomain = Utils.getHostname(uri); - const savedDomains: NeverDomains = { - ...existingDomains, - }; - savedDomains[validDomain] = null; - - await this.domainSettingsService.setNeverDomains(savedDomains); - - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("domainAddedToExcludedDomains", validDomain), - ); - } - - private abortSession(sessionId: string) { - BrowserFido2UserInterfaceSession.abortPopout(sessionId, true); - } -} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html deleted file mode 100644 index 8a052fbc5b7..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html +++ /dev/null @@ -1,142 +0,0 @@ - -
    -
    -
    - - - - - - -
    - - -
    - -
    -
    -
    - - - -
    -

    - {{ subtitleText | i18n }} -

    - - -
    -
    - -
    -
    - -
    - -
    -
    - - -
    - -
    -
    -
    -
    - -
    -

    {{ "passkeyAlreadyExists" | i18n }}

    -
    -
    - -
    -
    - -
    -
    - -
    -

    {{ "noPasskeysFoundForThisApplication" | i18n }}

    -
    - -
    -
    - - -
    -
    diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts deleted file mode 100644 index 8dc603cfe29..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts +++ /dev/null @@ -1,445 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - concatMap, - filter, - firstValueFrom, - map, - Observable, - Subject, - take, - takeUntil, -} from "rxjs"; - -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; -import { DialogService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; - -import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; -import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; -import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; -import { - BrowserFido2Message, - BrowserFido2UserInterfaceSession, - BrowserFido2MessageTypes, -} from "../../fido2/services/browser-fido2-user-interface.service"; - -interface ViewData { - message: BrowserFido2Message; - fallbackSupported: boolean; -} - -@Component({ - selector: "app-fido2-v1", - templateUrl: "fido2-v1.component.html", - styleUrls: [], -}) -export class Fido2V1Component implements OnInit, OnDestroy { - private destroy$ = new Subject(); - private hasSearched = false; - - protected cipher: CipherView; - protected searchTypeSearch = false; - protected searchPending = false; - protected searchText: string; - protected url: string; - protected hostname: string; - protected data$: Observable; - protected sessionId?: string; - protected senderTabId?: string; - protected ciphers?: CipherView[] = []; - protected displayedCiphers?: CipherView[] = []; - protected loading = false; - protected subtitleText: string; - protected credentialText: string; - protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; - - private message$ = new BehaviorSubject(null); - - constructor( - private router: Router, - private activatedRoute: ActivatedRoute, - private cipherService: CipherService, - private platformUtilsService: PlatformUtilsService, - private domainSettingsService: DomainSettingsService, - private searchService: SearchService, - private logService: LogService, - private dialogService: DialogService, - private browserMessagingApi: ZonedMessageListenerService, - private passwordRepromptService: PasswordRepromptService, - private fido2UserVerificationService: Fido2UserVerificationService, - private accountService: AccountService, - ) {} - - ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - - const queryParams$ = this.activatedRoute.queryParamMap.pipe( - take(1), - map((queryParamMap) => ({ - sessionId: queryParamMap.get("sessionId"), - senderTabId: queryParamMap.get("senderTabId"), - senderUrl: queryParamMap.get("senderUrl"), - })), - ); - - combineLatest([ - queryParams$, - this.browserMessagingApi.messageListener$() as Observable, - ]) - .pipe( - concatMap(async ([queryParams, message]) => { - this.sessionId = queryParams.sessionId; - this.senderTabId = queryParams.senderTabId; - this.url = queryParams.senderUrl; - // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. - if ( - message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && - message.sessionId !== queryParams.sessionId - ) { - this.abort(false); - return; - } - - // Ignore messages that don't belong to the current session. - if (message.sessionId !== queryParams.sessionId) { - return; - } - - if (message.type === BrowserFido2MessageTypes.AbortRequest) { - this.abort(false); - return; - } - - return message; - }), - filter((message) => !!message), - takeUntil(this.destroy$), - ) - .subscribe((message) => { - this.message$.next(message); - }); - - this.data$ = this.message$.pipe( - filter((message) => message != undefined), - concatMap(async (message) => { - switch (message.type) { - case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); - - this.ciphers = (await this.cipherService.getAllDecrypted()).filter( - (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, - ); - this.displayedCiphers = this.ciphers.filter( - (cipher) => - cipher.login.matchesUri(this.url, equivalentDomains) && - this.hasNoOtherPasskeys(cipher, message.userHandle), - ); - - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - - case BrowserFido2MessageTypes.PickCredentialRequest: { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.ciphers = await Promise.all( - message.cipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - }), - ); - this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - - case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.ciphers = await Promise.all( - message.existingCipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - }), - ); - this.displayedCiphers = [...this.ciphers]; - - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - } - - this.subtitleText = - this.displayedCiphers.length > 0 - ? this.getCredentialSubTitleText(message.type) - : "noMatchingPasskeyLogin"; - - this.credentialText = this.getCredentialButtonText(message.type); - return { - message, - fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, - }; - }), - takeUntil(this.destroy$), - ); - - queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { - this.send({ - sessionId: queryParams.sessionId, - type: BrowserFido2MessageTypes.ConnectResponse, - }); - }); - } - - protected async submit() { - const data = this.message$.value; - if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { - // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); - - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher.id, - type: BrowserFido2MessageTypes.PickCredentialResponse, - userVerified, - }); - } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - if (this.cipher.login.hasFido2Credentials) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "overwritePasskey" }, - content: { key: "overwritePasskeyAlert" }, - type: "info", - }); - - if (!confirmed) { - return false; - } - } - - // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); - - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher.id, - type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, - userVerified, - }); - } - - this.loading = true; - } - - protected async saveNewLogin() { - const data = this.message$.value; - if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - const name = data.credentialName || data.rpId; - // TODO: Revert to check for user verification once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - await this.createNewCipher(name, data.userName); - - // We are bypassing user verification pending approval. - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher?.id, - type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, - userVerified: data.userVerification, - }); - } - - this.loading = true; - } - - getCredentialSubTitleText(messageType: string): string { - return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest - ? "chooseCipherForPasskeySave" - : "logInWithPasskeyQuestion"; - } - - getCredentialButtonText(messageType: string): string { - return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest - ? "savePasskey" - : "confirm"; - } - - selectedPasskey(item: CipherView) { - this.cipher = item; - } - - viewPasskey() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { - queryParams: { - cipherId: this.cipher.id, - uilocation: "popout", - senderTabId: this.senderTabId, - sessionId: this.sessionId, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - addCipher() { - const data = this.message$.value; - - if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-cipher"], { - queryParams: { - name: data.credentialName || data.rpId, - uri: this.url, - type: CipherType.Login.toString(), - uilocation: "popout", - username: data.userName, - senderTabId: this.senderTabId, - sessionId: this.sessionId, - userVerification: data.userVerification, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - protected async search() { - this.hasSearched = await this.searchService.isSearchable(this.searchText); - this.searchPending = true; - if (this.hasSearched) { - this.displayedCiphers = await this.searchService.searchCiphers( - this.searchText, - null, - this.ciphers, - ); - } else { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); - this.displayedCiphers = this.ciphers.filter((cipher) => - cipher.login.matchesUri(this.url, equivalentDomains), - ); - } - this.searchPending = false; - this.selectedPasskey(this.displayedCiphers[0]); - } - - abort(fallback: boolean) { - this.unload(fallback); - window.close(); - } - - unload(fallback = false) { - this.send({ - sessionId: this.sessionId, - type: BrowserFido2MessageTypes.AbortResponse, - fallbackRequested: fallback, - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private buildCipher(name: string, username: string) { - this.cipher = new CipherView(); - this.cipher.name = name; - - this.cipher.type = CipherType.Login; - this.cipher.login = new LoginView(); - this.cipher.login.username = username; - this.cipher.login.uris = [new LoginUriView()]; - this.cipher.login.uris[0].uri = this.url; - this.cipher.card = new CardView(); - this.cipher.identity = new IdentityView(); - this.cipher.secureNote = new SecureNoteView(); - this.cipher.secureNote.type = SecureNoteType.Generic; - this.cipher.reprompt = CipherRepromptType.None; - } - - private async createNewCipher(name: string, username: string) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.buildCipher(name, username); - const cipher = await this.cipherService.encrypt(this.cipher, activeUserId); - try { - await this.cipherService.createWithServer(cipher); - this.cipher.id = cipher.id; - } catch (e) { - this.logService.error(e); - } - } - - // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. - private async handleUserVerification( - userVerificationRequested: boolean, - cipher: CipherView, - ): Promise { - const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0; - - if (masterPasswordRepromptRequired) { - return await this.passwordRepromptService.showPasswordPrompt(); - } - - return userVerificationRequested; - } - - private send(msg: BrowserFido2Message) { - BrowserFido2UserInterfaceSession.sendMessage({ - sessionId: this.sessionId, - ...msg, - }); - } - - /** - * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle - * @param userHandle - */ - private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { - if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { - return true; - } - - return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); - } -} diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html deleted file mode 100644 index 530519e88f1..00000000000 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html +++ /dev/null @@ -1,258 +0,0 @@ -
    -
    - -
    -

    - {{ "autofill" | i18n }} -

    -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - - -
    - -
    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    -

    {{ "additionalOptions" | i18n }}

    -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - - -
    -
    - -
    -
    diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts deleted file mode 100644 index 085ccba7e1e..00000000000 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts +++ /dev/null @@ -1,339 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { - InlineMenuVisibilitySetting, - ClearClipboardDelaySetting, -} from "@bitwarden/common/autofill/types"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { - UriMatchStrategy, - UriMatchStrategySetting, -} from "@bitwarden/common/models/domain/domain-service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { DialogService } from "@bitwarden/components"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; - -@Component({ - selector: "app-autofill-v1", - templateUrl: "autofill-v1.component.html", -}) -export class AutofillV1Component implements OnInit { - protected canOverrideBrowserAutofillSetting = false; - protected defaultBrowserAutofillDisabled = false; - protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; - protected autoFillOverlayVisibilityOptions: any[]; - protected disablePasswordManagerLink: string; - protected inlineMenuPositioningImprovementsEnabled: boolean = false; - protected showInlineMenuIdentities: boolean = true; - protected showInlineMenuCards: boolean = true; - inlineMenuIsEnabled: boolean = false; - enableAutoFillOnPageLoad = false; - autoFillOnPageLoadDefault = false; - autoFillOnPageLoadOptions: any[]; - enableContextMenuItem = false; - enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true? - clearClipboard: ClearClipboardDelaySetting; - clearClipboardOptions: any[]; - defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; - uriMatchOptions: any[]; - showCardsCurrentTab = false; - showIdentitiesCurrentTab = false; - autofillKeyboardHelperText: string; - accountSwitcherEnabled = false; - - constructor( - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private domainSettingsService: DomainSettingsService, - private configService: ConfigService, - private dialogService: DialogService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private messagingService: MessagingService, - private vaultSettingsService: VaultSettingsService, - ) { - this.autoFillOverlayVisibilityOptions = [ - { - name: i18nService.t("autofillOverlayVisibilityOff"), - value: AutofillOverlayVisibility.Off, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnFieldFocus"), - value: AutofillOverlayVisibility.OnFieldFocus, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnButtonClick"), - value: AutofillOverlayVisibility.OnButtonClick, - }, - ]; - this.autoFillOnPageLoadOptions = [ - { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, - { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, - ]; - this.clearClipboardOptions = [ - { name: i18nService.t("never"), value: null }, - { name: i18nService.t("tenSeconds"), value: 10 }, - { name: i18nService.t("twentySeconds"), value: 20 }, - { name: i18nService.t("thirtySeconds"), value: 30 }, - { name: i18nService.t("oneMinute"), value: 60 }, - { name: i18nService.t("twoMinutes"), value: 120 }, - { name: i18nService.t("fiveMinutes"), value: 300 }, - ]; - this.uriMatchOptions = [ - { name: i18nService.t("baseDomainOptionRecommended"), value: UriMatchStrategy.Domain }, - { name: i18nService.t("host"), value: UriMatchStrategy.Host }, - { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, - { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, - { name: i18nService.t("exact"), value: UriMatchStrategy.Exact }, - { name: i18nService.t("never"), value: UriMatchStrategy.Never }, - ]; - - this.accountSwitcherEnabled = enableAccountSwitching(); - this.disablePasswordManagerLink = this.getDisablePasswordManagerLink(); - } - - async ngOnInit() { - this.canOverrideBrowserAutofillSetting = - this.platformUtilsService.isChrome() || - this.platformUtilsService.isEdge() || - this.platformUtilsService.isOpera() || - this.platformUtilsService.isVivaldi(); - - this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden(); - - this.autoFillOverlayVisibility = await firstValueFrom( - this.autofillSettingsService.inlineMenuVisibility$, - ); - - this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag( - FeatureFlag.InlineMenuPositioningImprovements, - ); - - this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); - - this.showInlineMenuIdentities = - this.inlineMenuPositioningImprovementsEnabled && - (await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$)); - - this.showInlineMenuCards = - this.inlineMenuPositioningImprovementsEnabled && - (await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$)); - - this.enableAutoFillOnPageLoad = await firstValueFrom( - this.autofillSettingsService.autofillOnPageLoad$, - ); - - this.autoFillOnPageLoadDefault = await firstValueFrom( - this.autofillSettingsService.autofillOnPageLoadDefault$, - ); - - this.enableContextMenuItem = await firstValueFrom( - this.autofillSettingsService.enableContextMenu$, - ); - - this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); - - this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); - - const defaultUriMatch = await firstValueFrom( - this.domainSettingsService.defaultUriMatchStrategy$, - ); - this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch; - - const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); - await this.setAutofillKeyboardHelperText(command); - - this.showCardsCurrentTab = await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$); - - this.showIdentitiesCurrentTab = await firstValueFrom( - this.vaultSettingsService.showIdentitiesCurrentTab$, - ); - } - - isInlineMenuEnabled() { - return ( - this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnFieldFocus || - this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick - ); - } - - async updateAutoFillOverlayVisibility() { - await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); - await this.requestPrivacyPermission(); - - this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); - } - - async updateAutoFillOnPageLoad() { - await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad); - } - - async updateAutoFillOnPageLoadDefault() { - await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); - } - - async saveDefaultUriMatch() { - await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch); - } - - private async setAutofillKeyboardHelperText(command: string) { - if (command) { - this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command); - } else { - this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet"); - } - } - - async commandSettings() { - if (this.platformUtilsService.isChrome()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("chrome://extensions/shortcuts"); - } else if (this.platformUtilsService.isOpera()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("opera://extensions/shortcuts"); - } else if (this.platformUtilsService.isEdge()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("edge://extensions/shortcuts"); - } else if (this.platformUtilsService.isVivaldi()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("vivaldi://extensions/shortcuts"); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("https://bitwarden.com/help/keyboard-shortcuts"); - } - } - - private getDisablePasswordManagerLink(): string { - if (this.platformUtilsService.isChrome()) { - return "chrome://settings/autofill"; - } - if (this.platformUtilsService.isOpera()) { - return "opera://settings/autofill"; - } - if (this.platformUtilsService.isEdge()) { - return "edge://settings/passwords"; - } - if (this.platformUtilsService.isVivaldi()) { - return "vivaldi://settings/autofill"; - } - - return "https://bitwarden.com/help/disable-browser-autofill/"; - } - - protected openDisablePasswordManagerLink(event: Event) { - event.preventDefault(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab(this.disablePasswordManagerLink); - } - - async requestPrivacyPermission() { - if ( - this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off || - !this.canOverrideBrowserAutofillSetting || - (await this.browserAutofillSettingCurrentlyOverridden()) - ) { - return; - } - - await this.dialogService.openSimpleDialog({ - title: { key: "overrideDefaultBrowserAutofillTitle" }, - content: { key: "overrideDefaultBrowserAutofillDescription" }, - acceptButtonText: { key: "makeDefault" }, - acceptAction: async () => await this.handleOverrideDialogAccept(), - cancelButtonText: { key: "ignore" }, - type: "info", - }); - } - - async updateDefaultBrowserAutofillDisabled() { - const privacyPermissionGranted = await this.privacyPermissionGranted(); - if (!this.defaultBrowserAutofillDisabled && !privacyPermissionGranted) { - return; - } - - if ( - !privacyPermissionGranted && - !(await BrowserApi.requestPermission({ permissions: ["privacy"] })) - ) { - await this.dialogService.openSimpleDialog({ - title: { key: "privacyPermissionAdditionNotGrantedTitle" }, - content: { key: "privacyPermissionAdditionNotGrantedDescription" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "warning", - }); - this.defaultBrowserAutofillDisabled = false; - - return; - } - - BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled); - } - - private handleOverrideDialogAccept = async () => { - this.defaultBrowserAutofillDisabled = true; - await this.updateDefaultBrowserAutofillDisabled(); - }; - - async browserAutofillSettingCurrentlyOverridden() { - if (!this.canOverrideBrowserAutofillSetting) { - return false; - } - - if (!(await this.privacyPermissionGranted())) { - return false; - } - - return await BrowserApi.browserAutofillSettingsOverridden(); - } - - async privacyPermissionGranted(): Promise { - return await BrowserApi.permissionsGranted(["privacy"]); - } - - async updateContextMenuItem() { - await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem); - this.messagingService.send("bgUpdateContextMenu"); - } - - async updateAutoTotpCopy() { - await this.autofillSettingsService.setAutoCopyTotp(this.enableAutoTotpCopy); - } - - async saveClearClipboard() { - await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); - } - - async updateShowCardsCurrentTab() { - await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); - } - - async updateShowIdentitiesCurrentTab() { - await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); - } - - async updateShowInlineMenuCards() { - await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards); - } - - async updateShowInlineMenuIdentities() { - await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities); - } -} diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.html b/apps/browser/src/autofill/popup/settings/autofill.component.html index 18c6f515337..eeae0a85e3f 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.html +++ b/apps/browser/src/autofill/popup/settings/autofill.component.html @@ -120,7 +120,7 @@ /> {{ "showCardsInVaultView" | i18n }} - + + + + + {{ "clickToAutofillOnVault" | i18n }} + + @@ -270,5 +282,11 @@ + + + {{ "blockedDomains" | i18n }} + + + diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 828b45e7b37..884503fa360 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -49,7 +49,6 @@ import { import { BrowserApi } from "../../../platform/browser/browser-api"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; -import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -67,7 +66,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co JslibModule, LinkModule, PopOutComponent, - PopupFooterComponent, PopupHeaderComponent, PopupPageComponent, RouterModule, @@ -87,6 +85,7 @@ export class AutofillComponent implements OnInit { protected inlineMenuVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.OnFieldFocus; protected inlineMenuPositioningImprovementsEnabled: boolean = false; + protected blockBrowserInjectionsByDomainEnabled: boolean = false; protected browserClientVendor: BrowserClientVendor = BrowserClientVendors.Unknown; protected disablePasswordManagerURI: DisablePasswordManagerUri = DisablePasswordManagerUris.Unknown; @@ -110,6 +109,7 @@ export class AutofillComponent implements OnInit { uriMatchOptions: { name: string; value: UriMatchStrategySetting }[]; showCardsCurrentTab: boolean = true; showIdentitiesCurrentTab: boolean = true; + clickItemsVaultView: boolean = false; autofillKeyboardHelperText: string; accountSwitcherEnabled: boolean = false; @@ -163,6 +163,10 @@ export class AutofillComponent implements OnInit { FeatureFlag.InlineMenuPositioningImprovements, ); + this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag( + FeatureFlag.BlockBrowserInjectionsByDomain, + ); + this.showInlineMenuIdentities = this.inlineMenuPositioningImprovementsEnabled && (await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$)); @@ -207,6 +211,10 @@ export class AutofillComponent implements OnInit { this.showIdentitiesCurrentTab = await firstValueFrom( this.vaultSettingsService.showIdentitiesCurrentTab$, ); + + this.clickItemsVaultView = await firstValueFrom( + this.vaultSettingsService.clickItemsToAutofillVaultView$, + ); } async updateInlineMenuVisibility() { @@ -413,4 +421,8 @@ export class AutofillComponent implements OnInit { async updateShowInlineMenuIdentities() { await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities); } + + async updateClickItemsVaultView() { + await this.vaultSettingsService.setClickItemsToAutofillVaultView(this.clickItemsVaultView); + } } diff --git a/apps/browser/src/autofill/popup/settings/blocked-domains.component.html b/apps/browser/src/autofill/popup/settings/blocked-domains.component.html new file mode 100644 index 00000000000..bf5f40f2b90 --- /dev/null +++ b/apps/browser/src/autofill/popup/settings/blocked-domains.component.html @@ -0,0 +1,66 @@ + + + + + + + +
    +

    {{ "blockedDomainsDesc" | i18n }}

    + + +

    {{ "domainsTitle" | i18n }}

    + {{ blockedDomainsState?.length || 0 }} +
    + + + + + {{ + "websiteItemLabel" | i18n: i + 1 + }} + +
    {{ domain }}
    +
    + +
    +
    + +
    +
    + + + +
    diff --git a/apps/browser/src/autofill/popup/settings/blocked-domains.component.ts b/apps/browser/src/autofill/popup/settings/blocked-domains.component.ts new file mode 100644 index 00000000000..461f62da6dc --- /dev/null +++ b/apps/browser/src/autofill/popup/settings/blocked-domains.component.ts @@ -0,0 +1,208 @@ +import { CommonModule } from "@angular/common"; +import { + QueryList, + Component, + ElementRef, + OnDestroy, + AfterViewInit, + ViewChildren, +} from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { RouterModule } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + ButtonModule, + CardComponent, + FormFieldModule, + IconButtonModule, + ItemModule, + LinkModule, + SectionComponent, + SectionHeaderComponent, + ToastService, + TypographyModule, +} from "@bitwarden/components"; + +import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; + +@Component({ + selector: "app-blocked-domains", + templateUrl: "blocked-domains.component.html", + standalone: true, + imports: [ + ButtonModule, + CardComponent, + CommonModule, + FormFieldModule, + FormsModule, + IconButtonModule, + ItemModule, + JslibModule, + LinkModule, + PopOutComponent, + PopupFooterComponent, + PopupHeaderComponent, + PopupPageComponent, + RouterModule, + SectionComponent, + SectionHeaderComponent, + TypographyModule, + ], +}) +export class BlockedDomainsComponent implements AfterViewInit, OnDestroy { + @ViewChildren("uriInput") uriInputElements: QueryList> = + new QueryList(); + + dataIsPristine = true; + isLoading = false; + blockedDomainsState: string[] = []; + storedBlockedDomains: string[] = []; + // How many fields should be non-editable before editable fields + fieldsEditThreshold: number = 0; + + private destroy$ = new Subject(); + + constructor( + private domainSettingsService: DomainSettingsService, + private i18nService: I18nService, + private toastService: ToastService, + ) {} + + async ngAfterViewInit() { + this.domainSettingsService.blockedInteractionsUris$ + .pipe(takeUntil(this.destroy$)) + .subscribe((neverDomains: NeverDomains) => this.handleStateUpdate(neverDomains)); + + this.uriInputElements.changes.pipe(takeUntil(this.destroy$)).subscribe(({ last }) => { + this.focusNewUriInput(last); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + handleStateUpdate(neverDomains: NeverDomains) { + if (neverDomains) { + this.storedBlockedDomains = Object.keys(neverDomains); + } + + this.blockedDomainsState = [...this.storedBlockedDomains]; + + // Do not allow the first x (pre-existing) fields to be edited + this.fieldsEditThreshold = this.storedBlockedDomains.length; + + this.dataIsPristine = true; + this.isLoading = false; + } + + focusNewUriInput(elementRef: ElementRef) { + if (elementRef?.nativeElement) { + elementRef.nativeElement.focus(); + } + } + + async addNewDomain() { + // add empty field to the Domains list interface + this.blockedDomainsState.push(""); + + await this.fieldChange(); + } + + async removeDomain(i: number) { + this.blockedDomainsState.splice(i, 1); + + // If a pre-existing field was dropped, lower the edit threshold + if (i < this.fieldsEditThreshold) { + this.fieldsEditThreshold--; + } + + await this.fieldChange(); + } + + async fieldChange() { + if (this.dataIsPristine) { + this.dataIsPristine = false; + } + } + + async saveChanges() { + if (this.dataIsPristine) { + return; + } + + this.isLoading = true; + + const newBlockedDomainsSaveState: NeverDomains = {}; + const uniqueBlockedDomains = new Set(this.blockedDomainsState); + + for (const uri of uniqueBlockedDomains) { + if (uri && uri !== "") { + const validatedHost = Utils.getHostname(uri); + + if (!validatedHost) { + this.toastService.showToast({ + message: this.i18nService.t("excludedDomainsInvalidDomain", uri), + title: "", + variant: "error", + }); + + // Don't reset via `handleStateUpdate` to allow existing input value correction + this.isLoading = false; + return; + } + + newBlockedDomainsSaveState[validatedHost] = null; + } + } + + try { + const existingState = new Set(this.storedBlockedDomains); + const newState = new Set(Object.keys(newBlockedDomainsSaveState)); + const stateIsUnchanged = + existingState.size === newState.size && + new Set([...existingState, ...newState]).size === existingState.size; + + // The subscriber updates don't trigger if `setNeverDomains` sets an equivalent state + if (stateIsUnchanged) { + // Reset UI state directly + const constructedNeverDomainsState = this.storedBlockedDomains.reduce( + (neverDomains: NeverDomains, uri: string) => ({ ...neverDomains, [uri]: null }), + {}, + ); + this.handleStateUpdate(constructedNeverDomainsState); + } else { + await this.domainSettingsService.setBlockedInteractionsUris(newBlockedDomainsSaveState); + } + + this.toastService.showToast({ + message: this.i18nService.t("blockedDomainsSavedSuccess"), + title: "", + variant: "success", + }); + } catch { + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + title: "", + variant: "error", + }); + + // Don't reset via `handleStateUpdate` to preserve input values + this.isLoading = false; + } + } + + trackByFunction(index: number, _: string) { + return index; + } +} diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html deleted file mode 100644 index 8f78ac16404..00000000000 --- a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html +++ /dev/null @@ -1,91 +0,0 @@ -
    -
    -
    - -
    -

    - {{ "excludedDomains" | i18n }} -

    -
    - -
    -
    -
    -
    -
    - - -
    - -
    - - - - -
    -
    - -
    -
    -
    - -
    -
    -
    -
    diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts deleted file mode 100644 index 1a7dcd9c2dc..00000000000 --- a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts +++ /dev/null @@ -1,143 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; - -interface ExcludedDomain { - uri: string; - showCurrentUris: boolean; -} - -const BroadcasterSubscriptionId = "excludedDomains"; - -@Component({ - selector: "app-excluded-domains-v1", - templateUrl: "excluded-domains-v1.component.html", -}) -export class ExcludedDomainsV1Component implements OnInit, OnDestroy { - excludedDomains: ExcludedDomain[] = []; - existingExcludedDomains: ExcludedDomain[] = []; - currentUris: string[]; - loadCurrentUrisTimeout: number; - accountSwitcherEnabled = false; - - constructor( - private domainSettingsService: DomainSettingsService, - private i18nService: I18nService, - private router: Router, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); - if (savedDomains) { - for (const uri of Object.keys(savedDomains)) { - this.excludedDomains.push({ uri: uri, showCurrentUris: false }); - this.existingExcludedDomains.push({ uri: uri, showCurrentUris: false }); - } - } - - await this.loadCurrentUris(); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "tabChanged": - case "windowChanged": - if (this.loadCurrentUrisTimeout != null) { - window.clearTimeout(this.loadCurrentUrisTimeout); - } - this.loadCurrentUrisTimeout = window.setTimeout( - async () => await this.loadCurrentUris(), - 500, - ); - break; - default: - break; - } - }); - }); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - async addUri() { - this.excludedDomains.push({ uri: "", showCurrentUris: false }); - } - - async removeUri(i: number) { - this.excludedDomains.splice(i, 1); - } - - async submit() { - const savedDomains: { [name: string]: null } = {}; - const newExcludedDomains = this.getNewlyAddedDomains(this.excludedDomains); - for (const domain of this.excludedDomains) { - const resp = newExcludedDomains.filter((e) => e.uri === domain.uri); - if (resp.length === 0) { - savedDomains[domain.uri] = null; - } else { - if (domain.uri && domain.uri !== "") { - const validDomain = Utils.getHostname(domain.uri); - if (!validDomain) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("excludedDomainsInvalidDomain", domain.uri), - ); - return; - } - savedDomains[validDomain] = null; - } - } - } - - await this.domainSettingsService.setNeverDomains(savedDomains); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/tabs/settings"]); - } - - trackByFunction(index: number, item: any) { - return index; - } - - getNewlyAddedDomains(domain: ExcludedDomain[]): ExcludedDomain[] { - const result = this.excludedDomains.filter( - (newDomain) => - !this.existingExcludedDomains.some((oldDomain) => newDomain.uri === oldDomain.uri), - ); - return result; - } - - toggleUriInput(domain: ExcludedDomain) { - domain.showCurrentUris = !domain.showCurrentUris; - } - - async loadCurrentUris() { - const tabs = await BrowserApi.tabsQuery({ windowType: "normal" }); - if (tabs) { - const uriSet = new Set(tabs.map((tab) => Utils.getHostname(tab.url))); - uriSet.delete(null); - this.currentUris = Array.from(uriSet); - } - } -} diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts index 1391ad516fb..7d429bfe4f0 100644 --- a/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts +++ b/apps/browser/src/autofill/popup/settings/excluded-domains.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { QueryList, @@ -17,7 +15,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ButtonModule, @@ -28,6 +25,7 @@ import { LinkModule, SectionComponent, SectionHeaderComponent, + ToastService, TypographyModule, } from "@bitwarden/components"; @@ -62,7 +60,8 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co ], }) export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { - @ViewChildren("uriInput") uriInputElements: QueryList>; + @ViewChildren("uriInput") uriInputElements: QueryList> = + new QueryList(); accountSwitcherEnabled = false; dataIsPristine = true; @@ -77,7 +76,7 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { constructor( private domainSettingsService: DomainSettingsService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, ) { this.accountSwitcherEnabled = enableAccountSwitching(); } @@ -156,11 +155,11 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { const validatedHost = Utils.getHostname(uri); if (!validatedHost) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("excludedDomainsInvalidDomain", uri), - ); + this.toastService.showToast({ + message: this.i18nService.t("excludedDomainsInvalidDomain", uri), + title: "", + variant: "error", + }); // Don't reset via `handleStateUpdate` to allow existing input value correction this.isLoading = false; @@ -182,7 +181,7 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { if (stateIsUnchanged) { // Reset UI state directly const constructedNeverDomainsState = this.storedExcludedDomains.reduce( - (neverDomains, uri) => ({ ...neverDomains, [uri]: null }), + (neverDomains: NeverDomains, uri: string) => ({ ...neverDomains, [uri]: null }), {}, ); this.handleStateUpdate(constructedNeverDomainsState); @@ -190,13 +189,17 @@ export class ExcludedDomainsComponent implements AfterViewInit, OnDestroy { await this.domainSettingsService.setNeverDomains(newExcludedDomainsSaveState); } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("excludedDomainsSavedSuccess"), - ); + this.toastService.showToast({ + message: this.i18nService.t("excludedDomainsSavedSuccess"), + title: "", + variant: "success", + }); } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError")); + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + title: "", + variant: "error", + }); // Don't reset via `handleStateUpdate` to preserve input values this.isLoading = false; diff --git a/apps/browser/src/autofill/popup/settings/notifications-v1.component.html b/apps/browser/src/autofill/popup/settings/notifications-v1.component.html deleted file mode 100644 index 89d83c9e480..00000000000 --- a/apps/browser/src/autofill/popup/settings/notifications-v1.component.html +++ /dev/null @@ -1,89 +0,0 @@ -
    -
    - -
    -

    - {{ "notifications" | i18n }} -

    -
    - -
    -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    -
    diff --git a/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts b/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts deleted file mode 100644 index 442e91e55eb..00000000000 --- a/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; - -import { enableAccountSwitching } from "../../../platform/flags"; - -@Component({ - selector: "autofill-notification-v1-settings", - templateUrl: "notifications-v1.component.html", -}) -export class NotificationsSettingsV1Component implements OnInit { - enableAddLoginNotification = false; - enableChangedPasswordNotification = false; - enablePasskeys = true; - accountSwitcherEnabled = false; - - constructor( - private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, - private vaultSettingsService: VaultSettingsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - this.enableAddLoginNotification = await firstValueFrom( - this.userNotificationSettingsService.enableAddedLoginPrompt$, - ); - - this.enableChangedPasswordNotification = await firstValueFrom( - this.userNotificationSettingsService.enableChangedPasswordPrompt$, - ); - - this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); - } - - async updateAddLoginNotification() { - await this.userNotificationSettingsService.setEnableAddedLoginPrompt( - this.enableAddLoginNotification, - ); - } - - async updateChangedPasswordNotification() { - await this.userNotificationSettingsService.setEnableChangedPasswordPrompt( - this.enableChangedPasswordNotification, - ); - } - - async updateEnablePasskeys() { - await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys); - } -} 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 daa65d74ae6..acf0dedde27 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -957,8 +957,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ accountCreationFieldType: autofillFieldData?.accountCreationFieldType, }; + const allFields = this.formFieldElements; + const allFieldsRect = []; + + for (const key of allFields.keys()) { + const rect = await this.getMostRecentlyFocusedFieldRects(key); + allFieldsRect.push({ ...allFields.get(key), rect }); // Add the combined result to the array + } + await this.sendExtensionMessage("updateFocusedFieldData", { focusedFieldData: this.focusedFieldData, + allFieldsRect, }); } @@ -1408,6 +1417,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ url.origin + pathWithoutTrailingSlashAndSearch, url.origin + pathWithoutTrailingSlashSearchAndHash, ]); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_error) { return null; } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 812891930b8..16b11b98866 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -1,4 +1,4 @@ -import { mock, mockReset, MockProxy } from "jest-mock-extended"; +import { mock, MockProxy, mockReset } from "jest-mock-extended"; import { BehaviorSubject, of, Subject } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -98,7 +98,13 @@ describe("AutofillService", () => { let messageListener: MockProxy; beforeEach(() => { - scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); + configService = mock(); + configService.getFeatureFlag$.mockImplementation(() => of(false)); + scriptInjectorService = new BrowserScriptInjectorService( + domainSettingsService, + platformUtilsService, + logService, + ); inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); showInlineMenuCardsMock$ = new BehaviorSubject(false); showInlineMenuIdentitiesMock$ = new BehaviorSubject(false); @@ -106,10 +112,10 @@ describe("AutofillService", () => { autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; autofillSettingsService.showInlineMenuCards$ = showInlineMenuCardsMock$; autofillSettingsService.showInlineMenuIdentities$ = showInlineMenuIdentitiesMock$; + autofillSettingsService.autofillOnPageLoad$ = of(true); activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; - configService = mock(); messageListener = mock(); enableChangedPasswordPromptMock$ = new BehaviorSubject(true); enableAddedLoginPromptMock$ = new BehaviorSubject(true); @@ -132,7 +138,7 @@ describe("AutofillService", () => { userNotificationsSettings, messageListener, ); - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); jest.spyOn(BrowserApi, "tabSendMessage"); }); @@ -385,6 +391,7 @@ describe("AutofillService", () => { ); tabMock = createChromeTabMock(); sender = { tab: tabMock, frameId: 1 }; + jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock); jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); jest .spyOn(autofillService, "getInlineMenuVisibility") @@ -723,7 +730,9 @@ describe("AutofillService", () => { it("throws an error if an autofill did not occur for any of the passed pages", async () => { autofillOptions.tab.url = "https://a-different-url.com"; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(true)); try { await autofillService.doAutoFill(autofillOptions); @@ -905,7 +914,9 @@ describe("AutofillService", () => { it("returns a TOTP value", async () => { const totpCode = "123456"; autofillOptions.cipher.login.totp = "totp"; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(true)); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode); @@ -918,7 +929,9 @@ describe("AutofillService", () => { it("does not return a TOTP value if the user does not have premium features", async () => { autofillOptions.cipher.login.totp = "totp"; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(false)); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); const autofillResult = await autofillService.doAutoFill(autofillOptions); @@ -952,7 +965,9 @@ describe("AutofillService", () => { it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => { autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.organizationUseTotp = false; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(false)); const autofillResult = await autofillService.doAutoFill(autofillOptions); @@ -962,7 +977,9 @@ describe("AutofillService", () => { it("returns a null value if the user has disabled `auto TOTP copy`", async () => { autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.organizationUseTotp = true; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(true)); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false); jest.spyOn(totpService, "getCode"); @@ -2937,7 +2954,9 @@ describe("AutofillService", () => { const expectedDateFormats = [ ["mm/yyyy", "05/2024"], + ["mm/YYYY", "05/2024"], ["mm/yy", "05/24"], + ["MM/YY", "05/24"], ["yyyy/mm", "2024/05"], ["yy/mm", "24/05"], ["mm-yyyy", "05-2024"], @@ -2946,6 +2965,7 @@ describe("AutofillService", () => { ["yy-mm", "24-05"], ["yyyymm", "202405"], ["yymm", "2405"], + ["YYMM", "2405"], ["mmyyyy", "052024"], ["mmyy", "0524"], ]; @@ -3875,7 +3895,7 @@ describe("AutofillService", () => { }); describe("given a autofill field value that indicates the field is a `select` input", () => { - it("will not add an autofil action to the fill script if the dataValue cannot be found in the select options", () => { + it("will not add an autofill action to the fill script if the dataValue cannot be found in the select options", () => { const dataValue = "username"; const selectField = createAutofillFieldMock({ opid: "username-field", diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index d6524f7e70d..6d0e9954ade 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -416,8 +416,9 @@ export default class AutofillService implements AutofillServiceInterface { let totp: string | null = null; + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id), ); const defaultUriMatch = await this.getDefaultUriMatchStrategy(); @@ -1514,7 +1515,7 @@ export default class AutofillService implements AutofillServiceInterface { ); return CreditCardAutoFillConstants.CardAttributesExtended.find((attributeName) => { - const fieldAttributeValue = field[attributeName]; + const fieldAttributeValue = field[attributeName]?.toLocaleLowerCase(); const fieldAttributeMatch = fieldAttributeValue?.match(dateFormatPattern); // break find as soon as a match is found diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index 7471c298917..f15c3e4c389 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -21,7 +21,7 @@ jest.mock("../utils", () => { const utils = jest.requireActual("../utils"); return { ...utils, - debounce: jest.fn((fn) => fn), + debounce: jest.fn((fn, wait) => setTimeout(() => fn(), wait)), }; }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 13c546bccdb..8ccf726aa69 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -947,7 +947,8 @@ export class CollectAutofillContentService implements CollectAutofillContentServ } if (!this.mutationsQueue.length) { - requestIdleCallbackPolyfill(debounce(this.processMutations, 100), { timeout: 500 }); + // Collect all mutations and debounce the processing of those mutations by 100ms to ensure we don't process too many mutations at once. + debounce(this.processMutations, 100); } this.mutationsQueue.push(mutations); }; @@ -982,7 +983,8 @@ export class CollectAutofillContentService implements CollectAutofillContentServ const queueLength = this.mutationsQueue.length; if (!this.domQueryService.pageContainsShadowDomElements()) { - this.checkPageContainsShadowDom(); + // Checking if a page contains shadowDOM elements is a heavy operation and doesn't have to be done immediately, so we can call this within an idle moment on the event loop. + requestIdleCallbackPolyfill(this.checkPageContainsShadowDom, { timeout: 500 }); } for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) { diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts index 0dbc246b235..16310397a03 100644 --- a/apps/browser/src/autofill/services/dom-query.service.ts +++ b/apps/browser/src/autofill/services/dom-query.service.ts @@ -235,6 +235,8 @@ export class DomQueryService implements DomQueryServiceInterface { if ((chrome as any).dom?.openOrClosedShadowRoot) { try { return (chrome as any).dom.openOrClosedShadowRoot(node); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return null; } diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts index 6e267e25c3f..dc59e05a18b 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -21,8 +21,9 @@ describe("InlineMenuFieldQualificationService", () => { }); describe("isFieldForLoginForm", () => { - it("does not disqualify totp fields with flag set to true", () => { + it("does not disqualify totp fields for premium users with flag set to true", () => { inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true; + inlineMenuFieldQualificationService["premiumEnabled"] = true; const field = mock({ type: "text", autoCompleteType: "one-time-code", @@ -36,8 +37,25 @@ describe("InlineMenuFieldQualificationService", () => { ); }); - it("disqualify totp fields with flag set to false", () => { + it("disqualifies totp fields for premium users with flag set to false", () => { inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = false; + inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true; + const field = mock({ + type: "text", + autoCompleteType: "one-time-code", + htmlName: "totp", + htmlID: "totp", + placeholder: "totp", + }); + + expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( + false, + ); + }); + + it("disqualifies totp fields for non-premium users with flag set to true", () => { + inlineMenuFieldQualificationService["inlineMenuTotpFeatureFlag"] = true; + inlineMenuFieldQualificationService["premiumEnabled"] = false; const field = mock({ type: "text", autoCompleteType: "one-time-code", diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 32e55988a7a..43efd338c6e 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -151,14 +151,17 @@ export class InlineMenuFieldQualificationService private totpFieldAutocompleteValue = "one-time-code"; private inlineMenuFieldQualificationFlagSet = false; private inlineMenuTotpFeatureFlag = false; + private premiumEnabled = false; constructor() { void Promise.all([ sendExtensionMessage("getInlineMenuFieldQualificationFeatureFlag"), sendExtensionMessage("getInlineMenuTotpFeatureFlag"), - ]).then(([fieldQualificationFlag, totpFeatureFlag]) => { + sendExtensionMessage("getUserPremiumStatus"), + ]).then(([fieldQualificationFlag, totpFeatureFlag, premiumStatus]) => { this.inlineMenuFieldQualificationFlagSet = !!fieldQualificationFlag?.result; this.inlineMenuTotpFeatureFlag = !!totpFeatureFlag?.result; + this.premiumEnabled = !!premiumStatus?.result; }); } @@ -174,10 +177,11 @@ export class InlineMenuFieldQualificationService } /** - * Autofill does not fill password type totp input fields + * Totp inline menu is available only for premium users. */ - if (this.inlineMenuTotpFeatureFlag) { + if (this.inlineMenuTotpFeatureFlag && this.premiumEnabled) { const isTotpField = this.isTotpField(field); + // Autofill does not fill totp inputs with a "password" `type` attribute value const passwordType = field.type === "password"; if (isTotpField && !passwordType) { return true; diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 3a78e7f2c59..6e175906d30 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -241,7 +241,7 @@ export function createInitAutofillInlineMenuListMessageMock( createAutofillOverlayCipherDataMock(4, { icon: { imageEnabled: false, image: "", fallbackImage: "", icon: "" }, }), - createAutofillOverlayCipherDataMock(5, { login: { totp: "123456", totpField: true } }), + createAutofillOverlayCipherDataMock(5), createAutofillOverlayCipherDataMock(6), createAutofillOverlayCipherDataMock(7), createAutofillOverlayCipherDataMock(8), diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index a14ef1330cc..58ac95e7edf 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -1,5 +1,5 @@ +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; -import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; import { CipherType } from "@bitwarden/common/vault/enums"; diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 5922e26e11b..0e102dcfd99 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { FieldRect } from "../background/abstractions/overlay.background"; import { AutofillPort } from "../enums/autofill-port.enum"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; @@ -544,3 +545,31 @@ export const specialCharacterToKeyMap: Record = { "?": "questionCharacterDescriptor", "/": "forwardSlashCharacterDescriptor", }; + +/** + * Determines if the current rect values are not all 0. + */ +export function rectHasSize(rect: FieldRect): boolean { + if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) { + return true; + } + + return false; +} + +/** + * Checks if all the values corresponding to the specified keys in an object are null. + * If no keys are specified, checks all keys in the object. + * + * @param obj - The object to check. + * @param keys - An optional array of keys to check in the object. Defaults to all keys. + * @returns Returns true if all values for the specified keys (or all keys if none are provided) are null; otherwise, false. + */ +export function areKeyValuesNull>( + obj: T, + keys?: Array, +): boolean { + const keysToCheck = keys && keys.length > 0 ? keys : (Object.keys(obj) as Array); + + return keysToCheck.every((key) => obj[key] == null); +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d38e34991c9..4a7bbdb4ffc 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -204,6 +204,7 @@ import { BiometricStateService, BiometricsService, DefaultBiometricStateService, + DefaultKeyService, DefaultKdfConfigService, KdfConfigService, KeyService as KeyServiceAbstraction, @@ -241,7 +242,6 @@ import AutofillService from "../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service"; import { SafariApp } from "../browser/safariApp"; import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service"; -import { BrowserKeyService } from "../key-management/browser-key.service"; import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; @@ -268,7 +268,6 @@ import { OffscreenStorageService } from "../platform/storage/offscreen-storage.s import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; -import FilelessImporterBackground from "../tools/background/fileless-importer.background"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -393,7 +392,6 @@ export default class MainBackground { private notificationBackground: NotificationBackground; private overlayBackground: OverlayBackgroundInterface; private overlayNotificationsBackground: OverlayNotificationsBackgroundInterface; - private filelessImporterBackground: FilelessImporterBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; private webRequestBackground: WebRequestBackground; @@ -416,6 +414,7 @@ export default class MainBackground { await this.refreshMenu(true); if (this.systemService != null) { await this.systemService.clearPendingClipboard(); + await this.biometricsService.setShouldAutopromptNow(false); await this.processReloadService.startProcessReload(this.authService); } }; @@ -631,10 +630,6 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); - this.biometricsService = new BackgroundBrowserBiometricsService( - runtimeNativeMessagingBackground, - ); - this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider); this.pinService = new PinService( @@ -649,7 +644,7 @@ export default class MainBackground { this.stateService, ); - this.keyService = new BrowserKeyService( + this.keyService = new DefaultKeyService( this.pinService, this.masterPasswordService, this.keyGenerationService, @@ -660,11 +655,17 @@ export default class MainBackground { this.stateService, this.accountService, this.stateProvider, - this.biometricStateService, - this.biometricsService, this.kdfConfigService, ); + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + this.logService, + this.keyService, + this.biometricStateService, + this.messagingService, + ); + this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); @@ -695,7 +696,6 @@ export default class MainBackground { this.vaultTimeoutSettingsService, ); - this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); this.fileUploadService = new FileUploadService(this.logService, this.apiService); this.cipherFileUploadService = new CipherFileUploadService( this.apiService, @@ -770,7 +770,10 @@ export default class MainBackground { this.configService, ); - this.devicesService = new DevicesServiceImplementation(this.devicesApiService); + this.devicesService = new DevicesServiceImplementation( + this.devicesApiService, + this.appIdService, + ); this.authRequestService = new AuthRequestService( this.appIdService, @@ -793,6 +796,8 @@ export default class MainBackground { this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.stateProvider, + this.platformUtilsService, + this.apiService, ); this.ssoLoginService = new SsoLoginService(this.stateProvider); @@ -809,6 +814,11 @@ export default class MainBackground { this.authService, ); + this.domainSettingsService = new DefaultDomainSettingsService( + this.stateProvider, + this.configService, + ); + this.themeStateService = new DefaultThemeStateService( this.globalStateProvider, this.configService, @@ -848,10 +858,8 @@ export default class MainBackground { this.userVerificationApiService, this.userDecryptionOptionsService, this.pinService, - this.logService, - this.vaultTimeoutSettingsService, - this.platformUtilsService, this.kdfConfigService, + this.biometricsService, ); this.vaultFilterService = new VaultFilterService( @@ -881,6 +889,7 @@ export default class MainBackground { this.stateEventRunnerService, this.taskSchedulerService, this.logService, + this.biometricsService, lockedCallback, logoutCallback, ); @@ -957,6 +966,7 @@ export default class MainBackground { this.totpService = new TotpService(this.cryptoFunctionService, this.logService); this.scriptInjectorService = new BrowserScriptInjectorService( + this.domainSettingsService, this.platformUtilsService, this.logService, ); @@ -1071,6 +1081,7 @@ export default class MainBackground { this.vaultTimeoutSettingsService, this.biometricStateService, this.accountService, + this.logService, ); // Other fields @@ -1104,6 +1115,7 @@ export default class MainBackground { messageListener, this.accountService, lockService, + this.billingAccountProfileStateService, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.keyService, @@ -1146,16 +1158,6 @@ export default class MainBackground { this.notificationBackground, ); - this.filelessImporterBackground = new FilelessImporterBackground( - this.configService, - this.authService, - this.policyService, - this.notificationBackground, - this.importService, - this.syncService, - this.scriptInjectorService, - ); - this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( this.logService, this.autofillService, @@ -1223,6 +1225,7 @@ export default class MainBackground { this.i18nService, this.logService, this.billingAccountProfileStateService, + this.accountService, ); this.cipherContextMenuHandler = new CipherContextMenuHandler( @@ -1281,7 +1284,6 @@ export default class MainBackground { await this.runtimeBackground.init(); await this.notificationBackground.init(); this.overlayNotificationsBackground.init(); - this.filelessImporterBackground.init(); this.commandsBackground.init(); this.contextMenusBackground?.init(); this.idleBackground.init(); diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 2ded1760235..21c8f351e8d 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,10 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -14,18 +13,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey } from "@bitwarden/common/types/key"; -import { KeyService, BiometricStateService } from "@bitwarden/key-management"; +import { KeyService, BiometricStateService, BiometricsCommands } from "@bitwarden/key-management"; import { BrowserApi } from "../platform/browser/browser-api"; import RuntimeBackground from "./runtime.background"; const MessageValidTimeout = 10 * 1000; +const MessageNoResponseTimeout = 60 * 1000; const HashAlgorithmForEncryption = "sha1"; type Message = { command: string; + messageId?: number; // Filled in by this service userId?: string; @@ -43,6 +43,7 @@ type OuterMessage = { type ReceiveMessage = { timestamp: number; command: string; + messageId: number; response?: any; // Unlock key @@ -53,19 +54,23 @@ type ReceiveMessage = { type ReceiveMessageOuter = { command: string; appId: string; + messageId?: number; // Should only have one of these. message?: EncString; sharedSecret?: string; }; +type Callback = { + resolver: any; + rejecter: any; +}; + export class NativeMessagingBackground { - private connected = false; + connected = false; private connecting: boolean; private port: browser.runtime.Port | chrome.runtime.Port; - private resolver: any = null; - private rejecter: any = null; private privateKey: Uint8Array = null; private publicKey: Uint8Array = null; private secureSetupResolve: any = null; @@ -73,6 +78,11 @@ export class NativeMessagingBackground { private appId: string; private validatingFingerprint: boolean; + private messageId = 0; + private callbacks = new Map(); + + isConnectedToOutdatedDesktopClient = true; + constructor( private keyService: KeyService, private encryptService: EncryptService, @@ -97,6 +107,7 @@ export class NativeMessagingBackground { } async connect() { + this.logService.info("[Native Messaging IPC] Connecting to Bitwarden Desktop app..."); this.appId = await this.appIdService.getAppId(); await this.biometricStateService.setFingerprintValidated(false); @@ -106,6 +117,9 @@ export class NativeMessagingBackground { this.connecting = true; const connectedCallback = () => { + this.logService.info( + "[Native Messaging IPC] Connection to Bitwarden Desktop app established!", + ); this.connected = true; this.connecting = false; resolve(); @@ -123,11 +137,17 @@ export class NativeMessagingBackground { connectedCallback(); break; case "disconnected": + this.logService.info("[Native Messaging IPC] Disconnected from Bitwarden Desktop app."); if (this.connecting) { reject(new Error("startDesktop")); } this.connected = false; this.port.disconnect(); + // reject all + for (const callback of this.callbacks.values()) { + callback.rejecter("disconnected"); + } + this.callbacks.clear(); break; case "setupEncryption": { // Ignore since it belongs to another device @@ -142,11 +162,17 @@ export class NativeMessagingBackground { HashAlgorithmForEncryption, ); - if (this.validatingFingerprint) { - this.validatingFingerprint = false; - await this.biometricStateService.setFingerprintValidated(true); - } this.sharedSecret = new SymmetricCryptoKey(decrypted); + this.logService.info("[Native Messaging IPC] Secure channel established"); + + if ("messageId" in message) { + this.logService.info("[Native Messaging IPC] Non-legacy desktop client"); + this.isConnectedToOutdatedDesktopClient = false; + } else { + this.logService.info("[Native Messaging IPC] Legacy desktop client"); + this.isConnectedToOutdatedDesktopClient = true; + } + this.secureSetupResolve(); break; } @@ -155,28 +181,47 @@ export class NativeMessagingBackground { if (message.appId !== this.appId) { return; } + this.logService.warning( + "[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...", + ); this.sharedSecret = null; this.privateKey = null; this.connected = false; - this.rejecter({ - message: "invalidateEncryption", - }); + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId).rejecter({ + message: "invalidateEncryption", + }); + } return; case "verifyFingerprint": { - if (this.sharedSecret == null) { - this.validatingFingerprint = true; - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.showFingerprintDialog(); - } + this.logService.info("[Native Messaging IPC] Legacy app is requesting fingerprint"); + this.messagingService.send("showUpdateDesktopAppOrDisableFingerprintDialog", {}); + break; + } + case "verifyDesktopIPCFingerprint": { + this.logService.info( + "[Native Messaging IPC] Desktop app requested trust verification by fingerprint.", + ); + await this.showFingerprintDialog(); + break; + } + case "verifiedDesktopIPCFingerprint": { + await this.biometricStateService.setFingerprintValidated(true); + this.messagingService.send("hideNativeMessagingFingerprintDialog", {}); + break; + } + case "rejectedDesktopIPCFingerprint": { + this.messagingService.send("hideNativeMessagingFingerprintDialog", {}); break; } case "wrongUserId": - this.rejecter({ - message: "wrongUserId", - }); + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId).rejecter({ + message: "wrongUserId", + }); + } return; default: // Ignore since it belongs to another device @@ -210,6 +255,60 @@ export class NativeMessagingBackground { }); } + async callCommand(message: Message): Promise { + const messageId = this.messageId++; + + if ( + message.command == BiometricsCommands.Unlock || + message.command == BiometricsCommands.IsAvailable + ) { + // TODO remove after 2025.3 + // wait until there is no other callbacks, or timeout + const call = await firstValueFrom( + race( + from([false]).pipe(delay(5000)), + timer(0, 100).pipe( + filter(() => this.callbacks.size === 0), + map(() => true), + ), + ), + ); + if (!call) { + this.logService.info( + `[Native Messaging IPC] Message of type ${message.command} did not get a response before timing out`, + ); + return; + } + } + + const callback = new Promise((resolver, rejecter) => { + this.callbacks.set(messageId, { resolver, rejecter }); + }); + message.messageId = messageId; + try { + await this.send(message); + } catch (e) { + this.logService.info( + `[Native Messaging IPC] Error sending message of type ${message.command} to Bitwarden Desktop app. Error: ${e}`, + ); + const callback = this.callbacks.get(messageId); + this.callbacks.delete(messageId); + callback.rejecter("errorConnecting"); + } + + setTimeout(() => { + if (this.callbacks.has(messageId)) { + this.logService.info("[Native Messaging IPC] Message timed out and received no response"); + this.callbacks.get(messageId).rejecter({ + message: "timeout", + }); + this.callbacks.delete(messageId); + } + }, MessageNoResponseTimeout); + + return callback; + } + async send(message: Message) { if (!this.connected) { await this.connect(); @@ -233,20 +332,7 @@ export class NativeMessagingBackground { return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret); } - getResponse(): Promise { - return new Promise((resolve, reject) => { - this.resolver = function (response: any) { - resolve(response); - }; - this.rejecter = function (resp: any) { - reject({ - message: resp, - }); - }; - }); - } - - private postMessage(message: OuterMessage) { + private postMessage(message: OuterMessage, messageId?: number) { // Wrap in try-catch to when the port disconnected without triggering `onDisconnect`. try { const msg: any = message; @@ -261,14 +347,20 @@ export class NativeMessagingBackground { }; } this.port.postMessage(msg); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.logService.error("NativeMessaging port disconnected, disconnecting."); + this.logService.info( + "[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.", + ); this.sharedSecret = null; this.privateKey = null; this.connected = false; - this.rejecter("invalidateEncryption"); + if (this.callbacks.has(messageId)) { + this.callbacks.get(messageId).rejecter("invalidateEncryption"); + } } } @@ -285,90 +377,30 @@ export class NativeMessagingBackground { } if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { - this.logService.error("NativeMessage is to old, ignoring."); + this.logService.info("[Native Messaging IPC] Received an old native message, ignoring..."); return; } - switch (message.command) { - case "biometricUnlock": { - if ( - ["not available", "not enabled", "not supported", "not unlocked", "canceled"].includes( - message.response, - ) - ) { - this.rejecter(message.response); - return; - } + const messageId = message.messageId; - // Check for initial setup of biometric unlock - const enabled = await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$); - if (enabled === null || enabled === false) { - if (message.response === "unlocked") { - await this.biometricStateService.setBiometricUnlockEnabled(true); - } - break; - } - - // Ignore unlock if already unlocked - if ((await this.authService.getAuthStatus()) === AuthenticationStatus.Unlocked) { - break; - } - - if (message.response === "unlocked") { - try { - if (message.userKeyB64) { - const userKey = new SymmetricCryptoKey( - Utils.fromB64ToArray(message.userKeyB64), - ) as UserKey; - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const isUserKeyValid = await this.keyService.validateUserKey(userKey, activeUserId); - if (isUserKeyValid) { - await this.keyService.setUserKey(userKey, activeUserId); - } else { - this.logService.error("Unable to verify biometric unlocked userkey"); - await this.keyService.clearKeys(activeUserId); - this.rejecter("userkey wrong"); - return; - } - } else { - throw new Error("No key received"); - } - } catch (e) { - this.logService.error("Unable to set key: " + e); - this.rejecter("userkey wrong"); - return; - } - - // Verify key is correct by attempting to decrypt a secret - try { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.keyService.getFingerprint(userId); - } catch (e) { - this.logService.error("Unable to verify key: " + e); - await this.keyService.clearKeys(); - this.rejecter("userkey wrong"); - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.runtimeBackground.processMessage({ command: "unlocked" }); - } - break; - } - case "biometricUnlockAvailable": { - this.resolver(message); - break; - } - default: - this.logService.error("NativeMessage, got unknown command: " + message.command); - break; + if ( + message.command == BiometricsCommands.Unlock || + message.command == BiometricsCommands.IsAvailable + ) { + this.logService.info( + `[Native Messaging IPC] Received legacy message of type ${message.command}`, + ); + const messageId = this.callbacks.keys().next().value; + const resolver = this.callbacks.get(messageId); + this.callbacks.delete(messageId); + resolver.resolver(message); + return; } - if (this.resolver) { - this.resolver(message); + if (this.callbacks.has(messageId)) { + this.callbacks.get(messageId).resolver(message); + } else { + this.logService.info("[Native Messaging IPC] Received message without a callback", message); } } @@ -384,6 +416,7 @@ export class NativeMessagingBackground { command: "setupEncryption", publicKey: Utils.fromBufferToB64(publicKey), userId: userId, + messageId: this.messageId++, }); return new Promise((resolve, reject) => (this.secureSetupResolve = resolve)); @@ -400,12 +433,9 @@ export class NativeMessagingBackground { } private async showFingerprintDialog() { - const fingerprint = await this.keyService.getFingerprint( - (await firstValueFrom(this.accountService.activeAccount$))?.id, - this.publicKey, - ); + const fingerprint = await this.keyService.getFingerprint(this.appId, this.publicKey); - this.messagingService.send("showNativeMessagingFinterprintDialog", { + this.messagingService.send("showNativeMessagingFingerprintDialog", { fingerprint: fingerprint, }); } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index f91b6983a08..38bb2ec50c9 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -7,16 +7,18 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { BiometricsCommands } from "@bitwarden/key-management"; -import { MessageListener, isExternalMessage } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -50,6 +52,7 @@ export default class RuntimeBackground { private messageListener: MessageListener, private accountService: AccountService, private readonly lockService: LockService, + private billingAccountProfileStateService: BillingAccountProfileStateService, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -69,11 +72,14 @@ export default class RuntimeBackground { sendResponse: (response: any) => void, ) => { const messagesWithResponse = [ - "biometricUnlock", - "biometricUnlockAvailable", + BiometricsCommands.AuthenticateWithBiometrics, + BiometricsCommands.GetBiometricsStatus, + BiometricsCommands.UnlockWithBiometricsForUser, + BiometricsCommands.GetBiometricsStatusForUser, "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag", "getInlineMenuTotpFeatureFlag", + "getUserPremiumStatus", ]; if (messagesWithResponse.includes(msg.command)) { @@ -182,13 +188,17 @@ export default class RuntimeBackground { break; } break; - case "biometricUnlock": { - const result = await this.main.biometricsService.authenticateBiometric(); - return result; + case BiometricsCommands.AuthenticateWithBiometrics: { + return await this.main.biometricsService.authenticateWithBiometrics(); } - case "biometricUnlockAvailable": { - const result = await this.main.biometricsService.isBiometricUnlockAvailable(); - return result; + case BiometricsCommands.GetBiometricsStatus: { + return await this.main.biometricsService.getBiometricsStatus(); + } + case BiometricsCommands.UnlockWithBiometricsForUser: { + return await this.main.biometricsService.unlockWithBiometricsForUser(msg.userId); + } + case BiometricsCommands.GetBiometricsStatusForUser: { + return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId); } case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": { return await this.configService.getFeatureFlag( @@ -198,6 +208,15 @@ export default class RuntimeBackground { case "getInlineMenuFieldQualificationFeatureFlag": { return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification); } + case "getUserPremiumStatus": { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const result = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), + ); + return result; + } case "getInlineMenuTotpFeatureFlag": { return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuTotp); } @@ -240,9 +259,7 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(false); - if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { - await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); - } + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); break; } case "addToLockedVaultPendingNotifications": @@ -269,9 +286,7 @@ export default class RuntimeBackground { await this.configService.ensureConfigFetched(); await this.main.updateOverlayCiphers(); - if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { - await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); - } + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); } break; case "openPopup": diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts index f24de0633ac..da3d2c07e4b 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.ts +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts @@ -7,6 +7,7 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -19,9 +20,9 @@ import { DialogService, ItemModule, SectionComponent, + ToastService, } from "@bitwarden/components"; -import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; @@ -34,7 +35,6 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co ButtonModule, CardComponent, CommonModule, - CurrentAccountComponent, ItemModule, JslibModule, PopupPageComponent, @@ -58,6 +58,8 @@ export class PremiumV2Component extends BasePremiumComponent { dialogService: DialogService, environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + toastService: ToastService, + accountService: AccountService, ) { super( i18nService, @@ -68,6 +70,8 @@ export class PremiumV2Component extends BasePremiumComponent { dialogService, environmentService, billingAccountProfileStateService, + toastService, + accountService, ); // Support old price string. Can be removed in future once all translations are properly updated. diff --git a/apps/browser/src/billing/popup/settings/premium.component.html b/apps/browser/src/billing/popup/settings/premium.component.html deleted file mode 100644 index a8f9855e62d..00000000000 --- a/apps/browser/src/billing/popup/settings/premium.component.html +++ /dev/null @@ -1,72 +0,0 @@ -
    -
    - -
    -

    - {{ "premiumMembership" | i18n }} -

    -
    -
    -
    -
    - -

    {{ "premiumNotCurrentMember" | i18n }}

    -

    {{ "premiumSignUpAndGet" | i18n }}

    -
      -
    • - - {{ "ppremiumSignUpStorage" | i18n }} -
    • -
    • - - {{ "premiumSignUpTwoStepOptions" | i18n }} -
    • -
    • - - {{ "ppremiumSignUpReports" | i18n }} -
    • -
    • - - {{ "ppremiumSignUpTotp" | i18n }} -
    • -
    • - - {{ "ppremiumSignUpSupport" | i18n }} -
    • -
    • - - {{ "ppremiumSignUpFuture" | i18n }} -
    • -
    -

    {{ priceString }}

    - - -
    - -

    {{ "premiumCurrentMember" | i18n }}

    -

    {{ "premiumCurrentMemberThanks" | i18n }}

    - -
    -
    -
    diff --git a/apps/browser/src/billing/popup/settings/premium.component.ts b/apps/browser/src/billing/popup/settings/premium.component.ts deleted file mode 100644 index 70c8667646c..00000000000 --- a/apps/browser/src/billing/popup/settings/premium.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { CurrencyPipe, Location } from "@angular/common"; -import { Component } from "@angular/core"; - -import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; - -@Component({ - selector: "app-premium", - templateUrl: "premium.component.html", -}) -export class PremiumComponent extends BasePremiumComponent { - priceString: string; - - constructor( - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - apiService: ApiService, - configService: ConfigService, - logService: LogService, - private location: Location, - private currencyPipe: CurrencyPipe, - dialogService: DialogService, - environmentService: EnvironmentService, - billingAccountProfileStateService: BillingAccountProfileStateService, - ) { - super( - i18nService, - platformUtilsService, - apiService, - configService, - logService, - dialogService, - environmentService, - billingAccountProfileStateService, - ); - - // Support old price string. Can be removed in future once all translations are properly updated. - const thePrice = this.currencyPipe.transform(this.price, "$"); - // Safari extension crashes due to $1 appearing in the price string ($10.00). Escape the $ to fix. - const formattedPrice = this.platformUtilsService.isSafari() - ? thePrice.replace("$", "$$$") - : thePrice; - this.priceString = i18nService.t("premiumPrice", formattedPrice); - if (this.priceString.indexOf("%price%") > -1) { - this.priceString = this.priceString.replace("%price%", thePrice); - } - } - - goBack() { - this.location.back(); - } -} diff --git a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts index 0cd48c45938..3031134dc34 100644 --- a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts +++ b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts @@ -1,36 +1,172 @@ import { Injectable } from "@angular/core"; -import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { + BiometricsService, + BiometricsCommands, + BiometricsStatus, + KeyService, + BiometricStateService, +} from "@bitwarden/key-management"; -import { BrowserBiometricsService } from "./browser-biometrics.service"; +import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; +import { BrowserApi } from "../../platform/browser/browser-api"; @Injectable() -export class BackgroundBrowserBiometricsService extends BrowserBiometricsService { - constructor(private nativeMessagingBackground: () => NativeMessagingBackground) { +export class BackgroundBrowserBiometricsService extends BiometricsService { + constructor( + private nativeMessagingBackground: () => NativeMessagingBackground, + private logService: LogService, + private keyService: KeyService, + private biometricStateService: BiometricStateService, + private messagingService: MessagingService, + ) { super(); } - async authenticateBiometric(): Promise { - const responsePromise = this.nativeMessagingBackground().getResponse(); - await this.nativeMessagingBackground().send({ command: "biometricUnlock" }); - const response = await responsePromise; - return response.response === "unlocked"; + async authenticateWithBiometrics(): Promise { + try { + await this.ensureConnected(); + + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.Unlock, + }); + return response.response == "unlocked"; + } else { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.AuthenticateWithBiometrics, + }); + return response.response; + } + } catch (e) { + this.logService.info("Biometric authentication failed", e); + return false; + } } - async isBiometricUnlockAvailable(): Promise { - const responsePromise = this.nativeMessagingBackground().getResponse(); - await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" }); - const response = await responsePromise; - return response.response === "available"; + async getBiometricsStatus(): Promise { + if (!(await BrowserApi.permissionsGranted(["nativeMessaging"]))) { + return BiometricsStatus.NativeMessagingPermissionMissing; + } + + try { + await this.ensureConnected(); + + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.IsAvailable, + }); + const resp = + response.response == "available" + ? BiometricsStatus.Available + : BiometricsStatus.HardwareUnavailable; + return resp; + } else { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.GetBiometricsStatus, + }); + + if (response.response) { + return response.response; + } + } + return BiometricsStatus.Available; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return BiometricsStatus.DesktopDisconnected; + } } - async biometricsNeedsSetup(): Promise { + async unlockWithBiometricsForUser(userId: UserId): Promise { + try { + await this.ensureConnected(); + + // todo remove after 2025.3 + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.Unlock, + }); + if (response.response == "unlocked") { + const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64); + const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey; + if (await this.keyService.validateUserKey(userKey, userId)) { + await this.biometricStateService.setBiometricUnlockEnabled(true); + await this.keyService.setUserKey(userKey, userId); + // to update badge and other things + this.messagingService.send("switchAccount", { userId }); + return userKey; + } + } else { + return null; + } + } else { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.UnlockWithBiometricsForUser, + userId: userId, + }); + if (response.response) { + // In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled + const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64); + const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey; + if (await this.keyService.validateUserKey(userKey, userId)) { + await this.biometricStateService.setBiometricUnlockEnabled(true); + await this.keyService.setUserKey(userKey, userId); + // to update badge and other things + this.messagingService.send("switchAccount", { userId }); + return userKey; + } + } else { + return null; + } + } + } catch (e) { + this.logService.info("Biometric unlock for user failed", e); + throw new Error("Biometric unlock failed"); + } + + return null; + } + + async getBiometricsStatusForUser(id: UserId): Promise { + try { + await this.ensureConnected(); + + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + return await this.getBiometricsStatus(); + } + + return ( + await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.GetBiometricsStatusForUser, + userId: id, + }) + ).response; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return BiometricsStatus.DesktopDisconnected; + } + } + + // the first time we call, this might use an outdated version of the protocol, so we drop the response + private async ensureConnected() { + if (!this.nativeMessagingBackground().connected) { + await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.IsAvailable, + }); + } + } + + async getShouldAutopromptNow(): Promise { return false; } - async biometricsSupportsAutoSetup(): Promise { - return false; - } - - async biometricsSetup(): Promise {} + async setShouldAutopromptNow(value: boolean): Promise {} } diff --git a/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts deleted file mode 100644 index 7ffbed45415..00000000000 --- a/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BiometricsService } from "@bitwarden/key-management"; - -import { BrowserApi } from "../../platform/browser/browser-api"; - -@Injectable() -export abstract class BrowserBiometricsService extends BiometricsService { - async supportsBiometric() { - const platformInfo = await BrowserApi.getPlatformInfo(); - if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") { - return true; - } - return false; - } - - abstract authenticateBiometric(): Promise; - abstract isBiometricUnlockAvailable(): Promise; -} diff --git a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts index f50468c8b7a..d248a630cc6 100644 --- a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts +++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts @@ -1,34 +1,55 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricsCommands, BiometricsService, BiometricsStatus } from "@bitwarden/key-management"; + import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserBiometricsService } from "./browser-biometrics.service"; +export class ForegroundBrowserBiometricsService extends BiometricsService { + shouldAutopromptNow = true; -export class ForegroundBrowserBiometricsService extends BrowserBiometricsService { - async authenticateBiometric(): Promise { + async authenticateWithBiometrics(): Promise { const response = await BrowserApi.sendMessageWithResponse<{ result: boolean; error: string; - }>("biometricUnlock"); + }>(BiometricsCommands.AuthenticateWithBiometrics); if (!response.result) { throw response.error; } return response.result; } - async isBiometricUnlockAvailable(): Promise { + async getBiometricsStatus(): Promise { const response = await BrowserApi.sendMessageWithResponse<{ - result: boolean; + result: BiometricsStatus; error: string; - }>("biometricUnlockAvailable"); - return response.result && response.result === true; + }>(BiometricsCommands.GetBiometricsStatus); + return response.result; } - async biometricsNeedsSetup(): Promise { - return false; + async unlockWithBiometricsForUser(userId: UserId): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: UserKey; + error: string; + }>(BiometricsCommands.UnlockWithBiometricsForUser, { userId }); + if (!response.result) { + return null; + } + return SymmetricCryptoKey.fromString(response.result.keyB64) as UserKey; } - async biometricsSupportsAutoSetup(): Promise { - return false; + async getBiometricsStatusForUser(id: UserId): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: BiometricsStatus; + error: string; + }>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id }); + return response.result; } - async biometricsSetup(): Promise {} + async getShouldAutopromptNow(): Promise { + return this.shouldAutopromptNow; + } + async setShouldAutopromptNow(value: boolean): Promise { + this.shouldAutopromptNow = value; + } } diff --git a/apps/browser/src/key-management/browser-key.service.ts b/apps/browser/src/key-management/browser-key.service.ts deleted file mode 100644 index 0cc5f13a27e..00000000000 --- a/apps/browser/src/key-management/browser-key.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom } from "rxjs"; - -import { PinServiceAbstraction } from "@bitwarden/auth/common"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; -import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "@bitwarden/common/types/key"; -import { - KdfConfigService, - DefaultKeyService, - BiometricsService, - BiometricStateService, -} from "@bitwarden/key-management"; - -export class BrowserKeyService extends DefaultKeyService { - constructor( - pinService: PinServiceAbstraction, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - keyGenerationService: KeyGenerationService, - cryptoFunctionService: CryptoFunctionService, - encryptService: EncryptService, - platformUtilService: PlatformUtilsService, - logService: LogService, - stateService: StateService, - accountService: AccountService, - stateProvider: StateProvider, - private biometricStateService: BiometricStateService, - private biometricsService: BiometricsService, - kdfConfigService: KdfConfigService, - ) { - super( - pinService, - masterPasswordService, - keyGenerationService, - cryptoFunctionService, - encryptService, - platformUtilService, - logService, - stateService, - accountService, - stateProvider, - kdfConfigService, - ); - } - override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise { - if (keySuffix === KeySuffixOptions.Biometric) { - const biometricUnlockPromise = - userId == null - ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) - : this.biometricStateService.getBiometricUnlockEnabled(userId); - return await biometricUnlockPromise; - } - return super.hasUserKeyStored(keySuffix, userId); - } - - /** - * Browser doesn't store biometric keys, so we retrieve them from the desktop and return - * if we successfully saved it into memory as the User Key - * @returns the `UserKey` if the user passes a biometrics prompt, otherwise return `null`. - */ - protected override async getKeyFromStorage( - keySuffix: KeySuffixOptions, - userId?: UserId, - ): Promise { - if (keySuffix === KeySuffixOptions.Biometric) { - const biometricsResult = await this.biometricsService.authenticateBiometric(); - - if (!biometricsResult) { - return null; - } - - const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); - if (userKey) { - return userKey; - } - } - - return await super.getKeyFromStorage(keySuffix, userId); - } -} diff --git a/apps/browser/src/services/extension-lock-component.service.spec.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts similarity index 82% rename from apps/browser/src/services/extension-lock-component.service.spec.ts rename to apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts index a8a019662ef..92830c35aca 100644 --- a/apps/browser/src/services/extension-lock-component.service.spec.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts @@ -2,7 +2,6 @@ import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular"; import { PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -10,9 +9,15 @@ import { import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { KeyService, BiometricsService } from "@bitwarden/key-management"; +import { + KeyService, + BiometricsService, + BiometricsStatus, + BiometricStateService, +} from "@bitwarden/key-management"; +import { UnlockOptions } from "@bitwarden/key-management/angular"; -import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; +import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; import { ExtensionLockComponentService } from "./extension-lock-component.service"; @@ -26,6 +31,7 @@ describe("ExtensionLockComponentService", () => { let vaultTimeoutSettingsService: MockProxy; let keyService: MockProxy; let routerService: MockProxy; + let biometricStateService: MockProxy; beforeEach(() => { userDecryptionOptionsService = mock(); @@ -35,6 +41,7 @@ describe("ExtensionLockComponentService", () => { vaultTimeoutSettingsService = mock(); keyService = mock(); routerService = mock(); + biometricStateService = mock(); TestBed.configureTestingModule({ providers: [ @@ -67,6 +74,10 @@ describe("ExtensionLockComponentService", () => { provide: BrowserRouterService, useValue: routerService, }, + { + provide: BiometricStateService, + useValue: biometricStateService, + }, ], }); @@ -121,8 +132,7 @@ describe("ExtensionLockComponentService", () => { describe("getAvailableUnlockOptions$", () => { interface MockInputs { hasMasterPassword: boolean; - osSupportsBiometric: boolean; - biometricLockSet: boolean; + biometricsStatusForUser: BiometricsStatus; hasBiometricEncryptedUserKeyStored: boolean; platformSupportsSecureStorage: boolean; pinDecryptionAvailable: boolean; @@ -133,8 +143,7 @@ describe("ExtensionLockComponentService", () => { // MP + PIN + Biometrics available { hasMasterPassword: true, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: true, @@ -148,7 +157,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -156,8 +165,7 @@ describe("ExtensionLockComponentService", () => { // PIN + Biometrics available { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: true, @@ -171,7 +179,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -179,8 +187,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics available: user key stored with no secure storage { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: false, pinDecryptionAvailable: false, @@ -194,7 +201,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -202,8 +209,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics available: no user key stored with no secure storage { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: false, platformSupportsSecureStorage: false, pinDecryptionAvailable: false, @@ -217,7 +223,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -225,8 +231,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics not available: biometric lock not set { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: false, + biometricsStatusForUser: BiometricsStatus.UnlockNeeded, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: false, @@ -240,7 +245,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + biometricsStatus: BiometricsStatus.UnlockNeeded, }, }, ], @@ -248,8 +253,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics not available: user key not stored { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.NotEnabledInConnectedDesktopApp, hasBiometricEncryptedUserKeyStored: false, platformSupportsSecureStorage: true, pinDecryptionAvailable: false, @@ -263,7 +267,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + biometricsStatus: BiometricsStatus.NotEnabledInConnectedDesktopApp, }, }, ], @@ -271,8 +275,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics not available: OS doesn't support { hasMasterPassword: false, - osSupportsBiometric: false, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.HardwareUnavailable, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: false, @@ -286,7 +289,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem, + biometricsStatus: BiometricsStatus.HardwareUnavailable, }, }, ], @@ -304,12 +307,17 @@ describe("ExtensionLockComponentService", () => { ); // Biometrics - biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric); - vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet); + biometricsService.getBiometricsStatusForUser.mockResolvedValue( + mockInputs.biometricsStatusForUser, + ); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue( + mockInputs.hasBiometricEncryptedUserKeyStored, + ); keyService.hasUserKeyStored.mockResolvedValue(mockInputs.hasBiometricEncryptedUserKeyStored); platformUtilsService.supportsSecureStorage.mockReturnValue( mockInputs.platformSupportsSecureStorage, ); + biometricStateService.biometricUnlockEnabled$ = of(true); // PIN pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts new file mode 100644 index 00000000000..711ae4e378f --- /dev/null +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts @@ -0,0 +1,88 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { inject } from "@angular/core"; +import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs"; + +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + BiometricsService, + BiometricsStatus, + BiometricStateService, +} from "@bitwarden/key-management"; +import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; + +import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; +import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; + +export class ExtensionLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + private readonly biometricsService = inject(BiometricsService); + private readonly pinService = inject(PinServiceAbstraction); + private readonly routerService = inject(BrowserRouterService); + private readonly biometricStateService = inject(BiometricStateService); + + getPreviousUrl(): string | null { + return this.routerService.getPreviousUrl(); + } + + getBiometricsError(error: any): string | null { + const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes]; + + if (!biometricsError) { + return null; + } + + return biometricsError.description; + } + + async isWindowVisible(): Promise { + throw new Error("Method not implemented."); + } + + getBiometricsUnlockBtnText(): string { + return "unlockWithBiometrics"; + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return combineLatest([ + // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to + defer(async () => { + if (!(await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$))) { + return BiometricsStatus.NotEnabledLocally; + } else { + // TODO remove after 2025.3 + // remove after backward compatibility code for old biometrics ipc protocol is removed + const result: BiometricsStatus = (await Promise.race([ + this.biometricsService.getBiometricsStatusForUser(userId), + new Promise((resolve) => + setTimeout(() => resolve(BiometricsStatus.DesktopDisconnected), 1000), + ), + ])) as BiometricsStatus; + return result; + } + }), + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + defer(() => this.pinService.isPinDecryptionAvailable(userId)), + ]).pipe( + map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => { + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: pinDecryptionAvailable, + }, + biometrics: { + enabled: biometricsStatus === BiometricsStatus.Available, + biometricsStatus: biometricsStatus, + }, + }; + return unlockOpts; + }), + ); + } +} diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 32fa5135226..016bf6dfe4b 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": "__MSG_appName__", - "version": "2024.12.3", + "version": "2025.1.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -29,12 +29,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { @@ -140,7 +134,6 @@ }, "web_accessible_resources": [ "content/fido2-page-script.js", - "content/lp-suppress-import-download.js", "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 87a08bc89e3..104036140bd 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": "__MSG_appName__", - "version": "2024.12.3", + "version": "2025.1.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -30,12 +30,6 @@ "matches": ["*://*/*", "file:///*"], "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" - }, - { - "all_frames": false, - "js": ["content/lp-fileless-importer.js"], - "matches": ["https://lastpass.com/export.php"], - "run_at": "document_start" } ], "background": { diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 05ea3c84e9e..1d8ff65c17d 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -165,6 +165,21 @@ export class BrowserApi { }); } + /** + * Fetch the currently open browser tab + */ + static async getCurrentTab(): Promise | null { + if (BrowserApi.isManifestVersion(3)) { + return await chrome.tabs.getCurrent(); + } + + return new Promise((resolve) => + chrome.tabs.getCurrent((tab) => { + resolve(tab); + }), + ); + } + static async tabsQuery(options: chrome.tabs.QueryInfo): Promise { return new Promise((resolve) => { chrome.tabs.query(options, (tabs) => { @@ -443,7 +458,7 @@ export class BrowserApi { // and that prompts us to show a new tab, this apparently doesn't happen on sideloaded // extensions and only shows itself production scenarios. See: https://bitwarden.atlassian.net/browse/PM-12298 if (this.isSafariApi) { - self.location.reload(); + return self.location.reload(); } return chrome.runtime.reload(); } diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 383e982f065..2b1040bcd8a 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -11,13 +11,11 @@ import { GroupPolicyEnvironment } from "../admin-console/types/group-policy-envi import { BrowserApi } from "./browser/browser-api"; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = { accountSwitching?: boolean; } & SharedFlags; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = { managedEnvironment?: GroupPolicyEnvironment; } & SharedDevFlags; diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 4065f2e46d7..67fa920d18d 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -8,6 +8,8 @@ describe("OffscreenDocument", () => { const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); const consoleErrorSpy = jest.spyOn(console, "error"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("../offscreen-document/offscreen-document"); describe("init", () => { diff --git a/apps/browser/src/platform/popup/browser-popup-utils.ts b/apps/browser/src/platform/popup/browser-popup-utils.ts index 35fd2361014..33a1ff4016d 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.ts @@ -3,7 +3,7 @@ import { BrowserApi } from "../browser/browser-api"; import { ScrollOptions } from "./abstractions/browser-popup-utils.abstractions"; -import { PopupWidthOptions } from "./layout/popup-width.service"; +import { PopupWidthOptions } from "./layout/popup-size.service"; class BrowserPopupUtils { /** @@ -24,6 +24,22 @@ class BrowserPopupUtils { return BrowserPopupUtils.urlContainsSearchParams(win, "uilocation", "popout"); } + /** + * Check if the current popup view is open inside of the current browser tab + * (it is possible in Chrome to open the extension in a tab) + */ + static async isInTab() { + const tabId = (await BrowserApi.getCurrentTab())?.id; + + if (tabId === undefined || tabId === null) { + return false; + } + + const result = BrowserApi.getExtensionViews({ tabId, type: "tab" }); + + return result.length > 0; + } + /** * Identifies if the popup is within the single action popout. * diff --git a/apps/browser/src/platform/popup/components/pop-out.component.html b/apps/browser/src/platform/popup/components/pop-out.component.html index c3f1f8ca150..3097f6e30d3 100644 --- a/apps/browser/src/platform/popup/components/pop-out.component.html +++ b/apps/browser/src/platform/popup/components/pop-out.component.html @@ -1,9 +1,4 @@ - - - - + + @@ -277,8 +277,8 @@ class MockSettingsPageComponent {} - - + + @@ -301,6 +301,16 @@ class MockVaultSubpageComponent {} export default { title: "Browser/Popup Layout", component: PopupPageComponent, + parameters: { + chromatic: { + // Disable tests while we troubleshoot their flaky-ness + disableSnapshot: true, + }, + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-38889&t=k6OTDDPZOTtypRqo-11", + }, + }, decorators: [ moduleMetadata({ imports: [ diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts index 7b4665040fb..ca019c16bd7 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -8,7 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic templateUrl: "popup-page.component.html", standalone: true, host: { - class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-hidden", + class: "tw-h-full tw-flex tw-flex-col tw-overflow-y-hidden", }, imports: [CommonModule], }) diff --git a/apps/browser/src/platform/popup/layout/popup-width.service.ts b/apps/browser/src/platform/popup/layout/popup-size.service.ts similarity index 60% rename from apps/browser/src/platform/popup/layout/popup-width.service.ts rename to apps/browser/src/platform/popup/layout/popup-size.service.ts index d8706dbcbcf..3ae9a633cab 100644 --- a/apps/browser/src/platform/popup/layout/popup-width.service.ts +++ b/apps/browser/src/platform/popup/layout/popup-size.service.ts @@ -7,6 +7,8 @@ import { POPUP_STYLE_DISK, } from "@bitwarden/common/platform/state"; +import BrowserPopupUtils from "../browser-popup-utils"; + /** * * Value represents width in pixels @@ -25,10 +27,12 @@ const POPUP_WIDTH_KEY_DEF = new KeyDefinition(POPUP_STYLE_DISK }); /** - * Updates the extension popup width based on a user setting + * Handles sizing the popup based on available width/height, which can be affected by + * user default zoom level. + * Updates the extension popup width based on a user setting. **/ @Injectable({ providedIn: "root" }) -export class PopupWidthService { +export class PopupSizeService { private static readonly LocalStorageKey = "bw-popup-width"; private readonly state = inject(GlobalStateProvider).get(POPUP_WIDTH_KEY_DEF); @@ -41,15 +45,31 @@ export class PopupWidthService { } /** Begin listening for state changes */ - init() { + async init() { this.width$.subscribe((width: PopupWidthOption) => { - PopupWidthService.setStyle(width); - localStorage.setItem(PopupWidthService.LocalStorageKey, width); + PopupSizeService.setStyle(width); + localStorage.setItem(PopupSizeService.LocalStorageKey, width); }); + + const isInChromeTab = await BrowserPopupUtils.isInTab(); + + if (!BrowserPopupUtils.inPopup(window) || isInChromeTab) { + window.document.body.classList.add("body-full"); + } else if (window.innerHeight < 400) { + window.document.body.classList.add("body-xxs"); + } else if (window.innerHeight < 500) { + window.document.body.classList.add("body-xs"); + } else if (window.innerHeight < 600) { + window.document.body.classList.add("body-sm"); + } } private static setStyle(width: PopupWidthOption) { + if (!BrowserPopupUtils.inPopup(window)) { + return; + } const pxWidth = PopupWidthOptions[width] ?? PopupWidthOptions.default; + document.body.style.minWidth = `${pxWidth}px`; } @@ -57,7 +77,7 @@ export class PopupWidthService { * To keep the popup size from flickering on bootstrap, we store the width in `localStorage` so we can quickly & synchronously reference it. **/ static initBodyWidthFromLocalStorage() { - const storedValue = localStorage.getItem(PopupWidthService.LocalStorageKey); + const storedValue = localStorage.getItem(PopupSizeService.LocalStorageKey); this.setStyle(storedValue as any); } } diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts index d6ec3dfde96..b177497305b 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -1,8 +1,22 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; +import { + DomainSettingsService, + DefaultDomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + FakeStateProvider, + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { createChromeTabMock } from "../../autofill/spec/autofill-mocks"; import { BrowserApi } from "../browser/browser-api"; import { @@ -11,8 +25,19 @@ import { } from "./abstractions/script-injector.service"; import { BrowserScriptInjectorService } from "./browser-script-injector.service"; +const mockEquivalentDomains = [ + ["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"], + ["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"], + ["example.co.uk", "exampleapp.co.uk"], +]; + describe("ScriptInjectorService", () => { const tabId = 1; + const tabMock = createChromeTabMock({ id: tabId }); + const mockBlockedURI = new URL(tabMock.url); + jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); + jest.spyOn(BrowserApi, "isManifestVersion"); + const combinedManifestVersionFile = "content/autofill-init.js"; const mv2SpecificFile = "content/autofill-init-mv2.js"; const mv2Details = { file: mv2SpecificFile }; @@ -22,14 +47,29 @@ describe("ScriptInjectorService", () => { runAt: "document_start", }; const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + let scriptInjectorService: BrowserScriptInjectorService; - jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); - jest.spyOn(BrowserApi, "isManifestVersion"); - const platformUtilsService = mock(); const logService = mock(); + const platformUtilsService = mock(); + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + let configService: MockProxy; + let domainSettingsService: DomainSettingsService; beforeEach(() => { - scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); + jest.spyOn(BrowserApi, "getTab").mockImplementation(async () => tabMock); + configService = mock(); + configService.getFeatureFlag$.mockImplementation(() => of(false)); + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); + domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); + domainSettingsService.blockedInteractionsUris$ = of({}); + scriptInjectorService = new BrowserScriptInjectorService( + domainSettingsService, + platformUtilsService, + logService, + ); + jest.spyOn(scriptInjectorService as any, "buildInjectionDetails"); }); describe("inject", () => { @@ -71,6 +111,58 @@ describe("ScriptInjectorService", () => { { world: "ISOLATED" }, ); }); + + it("skips injecting the script in manifest v3 when the tab domain is a blocked domain", async () => { + domainSettingsService.blockedInteractionsUris$ = of({ [mockBlockedURI.host]: null }); + manifestVersionSpy.mockReturnValue(3); + + await expect(scriptInjectorService["buildInjectionDetails"]).not.toHaveBeenCalled(); + }); + + it("skips injecting the script in manifest v2 when the tab domain is a blocked domain", async () => { + domainSettingsService.blockedInteractionsUris$ = of({ [mockBlockedURI.host]: null }); + manifestVersionSpy.mockReturnValue(2); + + await expect(scriptInjectorService["buildInjectionDetails"]).not.toHaveBeenCalled(); + }); + + it("injects the script in manifest v2 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: "all_frames", + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + allFrames: true, + file: combinedManifestVersionFile, + }); + }); + + it("injects the script in manifest v3 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: 10, + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { ...sharedInjectDetails, frameId: 10, file: combinedManifestVersionFile }, + { world: "ISOLATED" }, + ); + }); }); describe("injection of mv2 specific details", () => { diff --git a/apps/browser/src/platform/services/browser-script-injector.service.ts b/apps/browser/src/platform/services/browser-script-injector.service.ts index 2a6b7397a6c..c2bace669dc 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.ts @@ -1,3 +1,6 @@ +import { firstValueFrom } from "rxjs"; + +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -12,7 +15,10 @@ import { } from "./abstractions/script-injector.service"; export class BrowserScriptInjectorService extends ScriptInjectorService { + blockedDomains: Set = null; + constructor( + private readonly domainSettingsService: DomainSettingsService, private readonly platformUtilsService: PlatformUtilsService, private readonly logService: LogService, ) { @@ -32,6 +38,28 @@ export class BrowserScriptInjectorService extends ScriptInjectorService { throw new Error("No file specified for script injection"); } + const tab = tabId && (await BrowserApi.getTab(tabId)); + const tabURL = tab?.url ? new URL(tab.url) : null; + + // Check if the tab URI is on the disabled URIs list + let injectionAllowedInTab = true; + const blockedDomains = await firstValueFrom( + this.domainSettingsService.blockedInteractionsUris$, + ); + + if (blockedDomains && tabURL?.hostname) { + const blockedDomainsSet = new Set(Object.keys(blockedDomains)); + + injectionAllowedInTab = !(tabURL && blockedDomainsSet.has(tabURL.hostname)); + } + + if (!injectionAllowedInTab) { + this.logService.warning( + `${injectDetails.file} was not injected because ${tabURL?.hostname || "the tab URI"} is on the user's blocked domains list.`, + ); + return; + } + const injectionDetails = this.buildInjectionDetails(injectDetails, file); if (BrowserApi.isManifestVersion(3)) { diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 949ecebde8a..4d6a403a18a 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -48,6 +48,8 @@ describe("LocalBackedSessionStorage", () => { localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); const result = await sut.get("test"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( encrypted, sessionKey, @@ -69,6 +71,8 @@ describe("LocalBackedSessionStorage", () => { localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); const result = await sut.get("test"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( encrypted, sessionKey, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index bdc0bed80c0..900212ddefa 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -198,6 +198,8 @@ export class LocalBackedSessionStorageService private compareValues(value1: T, value2: T): boolean { try { return compareValues(value1, value2); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.logService.error( `error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`, diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts index a2f0c78cd52..0499f34a4ae 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts @@ -17,6 +17,8 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // ignore } diff --git a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts index e1e921cc3a3..f5daff93815 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts @@ -3,7 +3,6 @@ import { Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -30,12 +29,12 @@ describe("ForegroundSyncService", () => { const cipherService = mock(); const collectionService = mock(); const apiService = mock(); - const accountService = mock(); + const accountService = mockAccountServiceWith(userId); const authService = mock(); const sendService = mock(); const sendApiService = mock(); const messageListener = mock(); - const stateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); + const stateProvider = new FakeStateProvider(accountService); const sut = new ForegroundSyncService( stateService, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 0a6ff456938..b831eef0baa 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -16,8 +16,7 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; +import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -25,7 +24,6 @@ import { LoginComponent, LoginSecondaryContentComponent, LockIcon, - LockComponent, LoginViaAuthRequestComponent, PasswordHintComponent, RegistrationFinishComponent, @@ -42,14 +40,13 @@ import { SsoComponent, TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { LockComponent } from "@bitwarden/key-management/angular"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, VaultIcons, } from "@bitwarden/vault"; -import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { EnvironmentComponent } from "../auth/popup/environment.component"; @@ -65,23 +62,18 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; -import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; -import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; -import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; -import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; +import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-domains.component"; import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; -import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; -import { PremiumComponent } from "../billing/popup/settings/premium.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; @@ -95,28 +87,16 @@ import { ExportBrowserV2Component } from "../tools/popup/settings/export/export- import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component"; import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component"; import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard"; -import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; -import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; -import { CollectionsComponent } from "../vault/popup/components/vault/collections.component"; -import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component"; -import { ShareComponent } from "../vault/popup/components/vault/share.component"; -import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; -import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; -import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component"; import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component"; +import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component"; -import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; -import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component"; -import { FoldersComponent } from "../vault/popup/settings/folders.component"; -import { SyncComponent } from "../vault/popup/settings/sync.component"; import { TrashComponent } from "../vault/popup/settings/trash.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; -import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { RouteElevation } from "./app-routing.animations"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; @@ -161,11 +141,12 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { + { path: "fido2", + component: Fido2Component, canActivate: [fido2AuthGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, ...twofactorRefactorSwap( TwoFactorComponent, AnonLayoutWrapperComponent, @@ -273,56 +254,43 @@ const routes: Routes = [ data: { elevation: 1 } satisfies RouteDataProperties, }, { - path: "ciphers", - component: VaultItemsComponent, - canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - ...extensionRefreshSwap(ViewComponent, ViewV2Component, { path: "view-cipher", + component: ViewV2Component, canActivate: [authGuard], data: { // Above "trash" elevation: 3, } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(PasswordHistoryComponent, PasswordHistoryV2Component, { + }, + { path: "cipher-password-history", + component: PasswordHistoryV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { + }, + { path: "add-cipher", + component: AddEditV2Component, canActivate: [authGuard, debounceNavigationGuard()], data: { elevation: 1 } satisfies RouteDataProperties, runGuardsAndResolvers: "always", - }), - ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { + }, + { path: "edit-cipher", + component: AddEditV2Component, canActivate: [authGuard, debounceNavigationGuard()], data: { // Above "trash" elevation: 3, } satisfies RouteDataProperties, runGuardsAndResolvers: "always", - }), - { - path: "share-cipher", - component: ShareComponent, - canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, }, { - path: "collections", - component: CollectionsComponent, - canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - ...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, { path: "attachments", + component: AttachmentsV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "generator", component: CredentialGeneratorComponent, @@ -347,70 +315,66 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(AutofillV1Component, AutofillComponent, { + { path: "autofill", + component: AutofillComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(AccountSecurityV1Component, AccountSecurityComponent, { + }, + { path: "account-security", + component: AccountSecurityComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { + }, + { path: "notifications", + component: NotificationsSettingsComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, { + }, + { path: "vault-settings", + component: VaultSettingsV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(FoldersComponent, FoldersV2Component, { + }, + { path: "folders", + component: FoldersV2Component, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, - }), - { - path: "add-folder", - component: FolderAddEditComponent, - canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, }, { - path: "edit-folder", - component: FolderAddEditComponent, + path: "blocked-domains", + component: BlockedDomainsComponent, canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, + data: { elevation: 2 } satisfies RouteDataProperties, }, { - path: "sync", - component: SyncComponent, - canActivate: [authGuard], - data: { elevation: 1 } satisfies RouteDataProperties, - }, - ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { path: "excluded-domains", + component: ExcludedDomainsComponent, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(PremiumComponent, PremiumV2Component, { + }, + { path: "premium", - component: PremiumComponent, + component: PremiumV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, { + }, + { path: "appearance", + component: AppearanceV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), - ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { + }, + { path: "clone-cipher", + component: AddEditV2Component, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "add-send", component: SendAddEditV2Component, @@ -593,7 +557,7 @@ const routes: Routes = [ children: [ { path: "signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { elevation: 1, pageIcon: RegistrationUserAddIcon, @@ -619,7 +583,7 @@ const routes: Routes = [ }, { path: "finish-signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationLockAltIcon, elevation: 1, @@ -669,7 +633,6 @@ const routes: Routes = [ children: [ { path: "set-password-jit", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { pageTitle: { @@ -686,7 +649,7 @@ const routes: Routes = [ { path: "assign-collections", component: AssignCollections, - canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")], + canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, { @@ -714,6 +677,7 @@ const routes: Routes = [ pageTitle: { key: "importantNotice", }, + hideFooter: true, }, }, { diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 3382e24689a..8147524d2f3 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -21,9 +21,9 @@ import { ToastOptions, ToastService, } from "@bitwarden/components"; +import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service"; -import { PopupWidthService } from "../platform/popup/layout/popup-width.service"; import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service"; import { initPopupClosedListener } from "../platform/services/popup-view-cache-background.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -42,7 +42,6 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn export class AppComponent implements OnInit, OnDestroy { private viewCacheService = inject(PopupViewCacheService); private compactModeService = inject(PopupCompactModeService); - private widthService = inject(PopupWidthService); private lastActivity: Date; private activeUserId: UserId; @@ -66,6 +65,8 @@ export class AppComponent implements OnInit, OnDestroy { private toastService: ToastService, private accountService: AccountService, private animationControlService: AnimationControlService, + private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, ) {} async ngOnInit() { @@ -73,7 +74,6 @@ export class AppComponent implements OnInit, OnDestroy { await this.viewCacheService.init(); this.compactModeService.init(); - this.widthService.init(); // Component states must not persist between closing and reopening the popup, otherwise they become dead objects // Clear them aggressively to make sure this doesn't occur @@ -103,7 +103,7 @@ export class AppComponent implements OnInit, OnDestroy { this.messageListener.allMessages$ .pipe( - tap((msg: any) => { + tap(async (msg: any) => { if (msg.command === "doneLoggingOut") { // TODO: PM-8544 - why do we call logout in the popup after receiving the doneLoggingOut message? Hasn't this already completeted logout? this.authService.logOut(async () => { @@ -120,6 +120,7 @@ export class AppComponent implements OnInit, OnDestroy { msg.command === "locked" && (msg.userId == null || msg.userId == this.activeUserId) ) { + await this.biometricsService.setShouldAutopromptNow(false); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["lock"]); @@ -127,16 +128,24 @@ export class AppComponent implements OnInit, OnDestroy { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.showDialog(msg); - } else if (msg.command === "showNativeMessagingFinterprintDialog") { + } else if (msg.command === "showNativeMessagingFingerprintDialog") { // TODO: Should be refactored to live in another service. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.showNativeMessagingFingerprintDialog(msg); + } else if (msg.command === "showUpdateDesktopAppOrDisableFingerprintDialog") { + // TODO: Should be refactored to live in another service. + await this.showDialog({ + title: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogTitle"), + content: this.i18nService.t("updateDesktopAppOrDisableFingerprintDialogMessage"), + type: "warning", + }); } else if (msg.command === "showToast") { this.toastService._showToast(msg); } else if (msg.command === "reloadProcess") { if (this.platformUtilsService.isSafari()) { - window.setTimeout(() => { + window.setTimeout(async () => { + await this.biometricStateService.updateLastProcessReload(); window.location.reload(); }, 2000); } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index b547078084b..15a898aef53 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -29,26 +29,14 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; -import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; -import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component"; -import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; -import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component"; -import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; -import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; -import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; -import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; -import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; -import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; -import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; -import { PremiumComponent } from "../billing/popup/settings/premium.component"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; import { HeaderComponent } from "../platform/popup/header.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; @@ -56,25 +44,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; -import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; -import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; -import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component"; -import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; -import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; -import { CollectionsComponent } from "../vault/popup/components/vault/collections.component"; -import { CurrentTabComponent } from "../vault/popup/components/vault/current-tab.component"; -import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component"; -import { ShareComponent } from "../vault/popup/components/vault/share.component"; -import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component"; -import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; -import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component"; -import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component"; -import { ViewComponent } from "../vault/popup/components/vault/view.component"; -import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; -import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; -import { FoldersComponent } from "../vault/popup/settings/folders.component"; -import { SyncComponent } from "../vault/popup/settings/sync.component"; -import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; @@ -108,10 +77,6 @@ import "../platform/popup/locales"; ScrollingModule, ServicesModule, DialogModule, - ExcludedDomainsComponent, - Fido2CipherRowComponent, - Fido2Component, - Fido2UseBrowserLinkComponent, FilePopoutCalloutComponent, AvatarModule, AccountComponent, @@ -129,52 +94,25 @@ import "../platform/popup/locales"; ExtensionAnonLayoutWrapperComponent, ], declarations: [ - ActionButtonsComponent, - AddEditComponent, - AddEditCustomFieldsComponent, AppComponent, - AttachmentsComponent, - CipherRowComponent, - VaultItemsComponent, - CollectionsComponent, ColorPasswordPipe, ColorPasswordCountPipe, - CurrentTabComponent, EnvironmentComponent, - ExcludedDomainsV1Component, - Fido2CipherRowV1Component, - Fido2UseBrowserLinkV1Component, - FolderAddEditComponent, - FoldersComponent, - VaultFilterComponent, HintComponent, HomeComponent, LoginViaAuthRequestComponentV1, LoginComponentV1, LoginDecryptionOptionsComponentV1, - NotificationsSettingsV1Component, - AppearanceComponent, - PasswordHistoryComponent, - PremiumComponent, RegisterComponent, SetPasswordComponent, - VaultSettingsComponent, - ShareComponent, SsoComponentV1, - SyncComponent, TabsV2Component, TwoFactorComponent, TwoFactorOptionsComponent, UpdateTempPasswordComponent, UserVerificationComponent, - AccountSecurityComponentV1, VaultTimeoutInputComponent, - ViewComponent, - ViewCustomFieldsComponent, RemovePasswordComponent, - VaultSelectComponent, - Fido2V1Component, - AutofillV1Component, EnvironmentSelectorComponent, ], exports: [], 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 c860ef1e342..2a5a56e7dfc 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 @@ -1,7 +1,9 @@ -import { DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { filter, Subject, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { MessageListener } from "@bitwarden/common/platform/messaging"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; export type DesktopSyncVerificationDialogParams = { @@ -13,8 +15,30 @@ export type DesktopSyncVerificationDialogParams = { standalone: true, imports: [JslibModule, ButtonModule, DialogModule], }) -export class DesktopSyncVerificationDialogComponent { - constructor(@Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams) {} +export class DesktopSyncVerificationDialogComponent implements OnDestroy, OnInit { + private destroy$ = new Subject(); + + constructor( + @Inject(DIALOG_DATA) protected params: DesktopSyncVerificationDialogParams, + private dialogRef: DialogRef, + private messageListener: MessageListener, + ) {} + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + ngOnInit(): void { + this.messageListener.allMessages$ + .pipe( + filter((m) => m.command === "hideNativeMessagingFingerprintDialog"), + takeUntil(this.destroy$), + ) + .subscribe(() => { + this.dialogRef.close(); + }); + } static open(dialogService: DialogService, data: DesktopSyncVerificationDialogParams) { return dialogService.open(DesktopSyncVerificationDialogComponent, { diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts index db634ea2e2c..dadd7917b99 100644 --- a/apps/browser/src/popup/main.ts +++ b/apps/browser/src/popup/main.ts @@ -1,16 +1,18 @@ import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; -import { PopupWidthService } from "../platform/popup/layout/popup-width.service"; +import { PopupSizeService } from "../platform/popup/layout/popup-size.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./scss/popup.scss"); +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./scss/tailwind.css"); import { AppModule } from "./app.module"; // We put these first to minimize the delay in window changing. -PopupWidthService.initBodyWidthFromLocalStorage(); +PopupSizeService.initBodyWidthFromLocalStorage(); // Should be removed once we deprecate support for Safari 16.0 and older. See Jira ticket [PM-1861] if (BrowserPlatformUtilsService.shouldApplySafariHeightFix(window)) { document.documentElement.classList.add("safari_height_fix"); diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 49847dd7778..554396fa038 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -19,8 +19,8 @@ body { } body { - min-width: 380px; - height: 600px !important; + width: 380px; + height: 600px; position: relative; min-height: 100vh; overflow: hidden; @@ -33,18 +33,20 @@ body { } &.body-sm { - width: 375px !important; - height: 500px !important; + height: 500px; } &.body-xs { - width: 375px !important; - height: 300px !important; + height: 400px; + } + + &.body-xxs { + height: 300px; } &.body-full { - width: 100% !important; - height: 100% !important; + width: 100%; + height: 100%; } } diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 9e6471eaf28..24661438495 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -1,5 +1,5 @@ import { DOCUMENT } from "@angular/common"; -import { Inject, Injectable } from "@angular/core"; +import { inject, Inject, Injectable } from "@angular/core"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -10,8 +10,11 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; +import { PopupSizeService } from "../../platform/popup/layout/popup-size.service"; @Injectable() export class InitService { + private sizeService = inject(PopupSizeService); + constructor( private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, @@ -28,13 +31,7 @@ export class InitService { await this.i18nService.init(); this.twoFactorService.init(); - if (!BrowserPopupUtils.inPopup(window)) { - window.document.body.classList.add("body-full"); - } else if (window.screen.availHeight < 600) { - window.document.body.classList.add("body-xs"); - } else if (window.screen.availHeight <= 800) { - window.document.body.classList.add("body-sm"); - } + await this.sizeService.init(); const htmlEl = window.document.documentElement; this.themingService.applyThemeChangesTo(this.document); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 5b27833636f..24d82ab8b67 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -24,7 +24,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { AnonLayoutWrapperDataService, LoginComponentService, - LockComponentService, SsoComponentService, LoginDecryptionOptionsService, } from "@bitwarden/auth/angular"; @@ -112,9 +111,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac import { KdfConfigService, KeyService, - BiometricStateService, BiometricsService, + DefaultKeyService, } from "@bitwarden/key-management"; +import { LockComponentService } from "@bitwarden/key-management/angular"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; @@ -126,7 +126,7 @@ import { AutofillService as AutofillServiceAbstraction } from "../../autofill/se import AutofillService from "../../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service"; import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics"; -import { BrowserKeyService } from "../../key-management/browser-key.service"; +import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; /* eslint-disable no-restricted-imports */ @@ -150,7 +150,6 @@ import { BrowserStorageServiceProvider } from "../../platform/storage/browser-st import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { ForegroundSyncService } from "../../platform/sync/foreground-sync.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; -import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service"; @@ -232,11 +231,9 @@ const safeProviders: SafeProvider[] = [ stateService: StateService, accountService: AccountServiceAbstraction, stateProvider: StateProvider, - biometricStateService: BiometricStateService, - biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) => { - const keyService = new BrowserKeyService( + const keyService = new DefaultKeyService( pinService, masterPasswordService, keyGenerationService, @@ -247,8 +244,6 @@ const safeProviders: SafeProvider[] = [ stateService, accountService, stateProvider, - biometricStateService, - biometricsService, kdfConfigService, ); new ContainerService(keyService, encryptService).attachToGlobal(self); @@ -265,8 +260,6 @@ const safeProviders: SafeProvider[] = [ StateService, AccountServiceAbstraction, StateProvider, - BiometricStateService, - BiometricsService, KdfConfigService, ], }), @@ -327,7 +320,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DomainSettingsService, useClass: DefaultDomainSettingsService, - deps: [StateProvider], + deps: [StateProvider, ConfigService], }), safeProvider({ provide: AbstractStorageService, @@ -365,7 +358,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: ScriptInjectorService, useClass: BrowserScriptInjectorService, - deps: [PlatformUtilsService, LogService], + deps: [DomainSettingsService, PlatformUtilsService, LogService], }), safeProvider({ provide: VaultTimeoutService, @@ -574,7 +567,7 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: SdkClientFactory, - useFactory: (logService) => + useFactory: (logService: LogService) => flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(), deps: [LogService], }), diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift index 1768ce6b15f..58d95f959be 100644 --- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift +++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift @@ -86,8 +86,203 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { context.completeRequest(returningItems: [response], completionHandler: nil) } return - case "biometricUnlock": + case "authenticateWithBiometrics": + let messageId = message?["messageId"] as? Int + let laContext = LAContext() + guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "authenticateWithBiometrics", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + break + } + laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "authenticate") { (success, error) in + if success { + response.userInfo = [ SFExtensionMessageKey: [ + "message": [ + "command": "authenticateWithBiometrics", + "response": true, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ]] + } else { + response.userInfo = [ SFExtensionMessageKey: [ + "message": [ + "command": "authenticateWithBiometrics", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ]] + } + context.completeRequest(returningItems: [response], completionHandler: nil) + } + return + case "getBiometricsStatus": + let messageId = message?["messageId"] as? Int + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatus", + "response": BiometricsStatus.Available.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil); + break + case "unlockWithBiometricsForUser": + let messageId = message?["messageId"] as? Int + var error: NSError? + let laContext = LAContext() + + laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + + if let e = error, e.code != kLAErrorBiometryLockout { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil) + break + } + + guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else { + let messageId = message?["messageId"] as? Int + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil) + break + } + laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "unlock your vault") { (success, error) in + if success { + guard let userId = message?["userId"] as? String else { + return + } + let passwordName = userId + "_user_biometric" + var passwordLength: UInt32 = 0 + var passwordPtr: UnsafeMutableRawPointer? = nil + + var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil) + if status != errSecSuccess { + let fallbackName = "key" + status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil) + } + + if status == errSecSuccess { + let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String? + SecKeychainItemFreeContent(nil, passwordPtr) + + response.userInfo = [ SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": true, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "userKeyB64": result!.replacingOccurrences(of: "\"", with: ""), + "messageId": messageId, + ], + ]] + } else { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": true, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + } + } + + context.completeRequest(returningItems: [response], completionHandler: nil) + } + return + case "getBiometricsStatusForUser": + let messageId = message?["messageId"] as? Int + let laContext = LAContext() + if !laContext.isBiometricsAvailable() { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatusForUser", + "response": BiometricsStatus.HardwareUnavailable.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil) + break + } + + guard let userId = message?["userId"] as? String else { + return + } + let passwordName = userId + "_user_biometric" + var passwordLength: UInt32 = 0 + var passwordPtr: UnsafeMutableRawPointer? = nil + + var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil) + if status != errSecSuccess { + let fallbackName = "key" + status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil) + } + + if status == errSecSuccess { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatusForUser", + "response": BiometricsStatus.Available.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + } else { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatusForUser", + "response": BiometricsStatus.NotEnabledInConnectedDesktopApp.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + } + break + case "biometricUnlock": + var error: NSError? let laContext = LAContext() if(!laContext.isBiometricsAvailable()){ @@ -115,7 +310,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { ] break } - laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Bitwarden Safari Extension") { (success, error) in + laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Biometric Unlock") { (success, error) in if success { guard let userId = message?["userId"] as? String else { return @@ -157,7 +352,6 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { context.completeRequest(returningItems: [response], completionHandler: nil) } - return case "biometricUnlockAvailable": let laContext = LAContext() @@ -228,3 +422,15 @@ class DownloadFileMessage: Decodable, Encodable { class DownloadFileMessageBlobOptions: Decodable, Encodable { var type: String? } + +enum BiometricsStatus : Int { + case Available = 0 + case UnlockNeeded = 1 + case HardwareUnavailable = 2 + case AutoSetupNeeded = 3 + case ManualSetupNeeded = 4 + case PlatformUnsupported = 5 + case DesktopDisconnected = 6 + case NotEnabledLocally = 7 + case NotEnabledInConnectedDesktopApp = 8 +} diff --git a/apps/browser/src/services/extension-lock-component.service.ts b/apps/browser/src/services/extension-lock-component.service.ts deleted file mode 100644 index 4fc2ef69b21..00000000000 --- a/apps/browser/src/services/extension-lock-component.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { inject } from "@angular/core"; -import { combineLatest, defer, map, Observable } from "rxjs"; - -import { - BiometricsDisableReason, - LockComponentService, - UnlockOptions, -} from "@bitwarden/auth/angular"; -import { - PinServiceAbstraction, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; -import { UserId } from "@bitwarden/common/types/guid"; -import { KeyService, BiometricsService } from "@bitwarden/key-management"; - -import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors"; -import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; - -export class ExtensionLockComponentService implements LockComponentService { - private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); - private readonly platformUtilsService = inject(PlatformUtilsService); - private readonly biometricsService = inject(BiometricsService); - private readonly pinService = inject(PinServiceAbstraction); - private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); - private readonly keyService = inject(KeyService); - private readonly routerService = inject(BrowserRouterService); - - getPreviousUrl(): string | null { - return this.routerService.getPreviousUrl(); - } - - getBiometricsError(error: any): string | null { - const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes]; - - if (!biometricsError) { - return null; - } - - return biometricsError.description; - } - - async isWindowVisible(): Promise { - throw new Error("Method not implemented."); - } - - getBiometricsUnlockBtnText(): string { - return "unlockWithBiometrics"; - } - - private async isBiometricLockSet(userId: UserId): Promise { - const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId); - const hasBiometricEncryptedUserKeyStored = await this.keyService.hasUserKeyStored( - KeySuffixOptions.Biometric, - userId, - ); - const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); - - return ( - biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage) - ); - } - - private getBiometricsDisabledReason( - osSupportsBiometric: boolean, - biometricLockSet: boolean, - ): BiometricsDisableReason | null { - if (!osSupportsBiometric) { - return BiometricsDisableReason.NotSupportedOnOperatingSystem; - } else if (!biometricLockSet) { - return BiometricsDisableReason.EncryptedKeysUnavailable; - } - - return null; - } - - getAvailableUnlockOptions$(userId: UserId): Observable { - return combineLatest([ - // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to - defer(() => this.biometricsService.supportsBiometric()), - defer(() => this.isBiometricLockSet(userId)), - this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), - defer(() => this.pinService.isPinDecryptionAvailable(userId)), - ]).pipe( - map( - ([ - supportsBiometric, - isBiometricsLockSet, - userDecryptionOptions, - pinDecryptionAvailable, - ]) => { - const disableReason = this.getBiometricsDisabledReason( - supportsBiometric, - isBiometricsLockSet, - ); - - const unlockOpts: UnlockOptions = { - masterPassword: { - enabled: userDecryptionOptions.hasMasterPassword, - }, - pin: { - enabled: pinDecryptionAvailable, - }, - biometrics: { - enabled: supportsBiometric && isBiometricsLockSet, - disableReason: disableReason, - }, - }; - return unlockOpts; - }, - ), - ); - } -} diff --git a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts index 6270b3503b5..0d49595d2eb 100644 --- a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts +++ b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/src/abstractions/vault-timeout/vault-timeout.service"; -import { MessagingService } from "@bitwarden/common/src/platform/abstractions/messaging.service"; -import { UserId } from "@bitwarden/common/src/types/guid"; +import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { UserId } from "@bitwarden/common/types/guid"; export class ForegroundVaultTimeoutService implements BaseVaultTimeoutService { constructor(protected messagingService: MessagingService) {} diff --git a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts deleted file mode 100644 index 2ade5bf7672..00000000000 --- a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums"; - -type FilelessImportPortMessage = { - command?: string; - importType?: FilelessImportTypeKeys; - data?: string; -}; - -type FilelessImportPortMessageHandlerParams = { - message: FilelessImportPortMessage; - port: chrome.runtime.Port; -}; - -type ImportNotificationMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - cancelFilelessImport: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; -}; - -type LpImporterMessageHandlers = { - [key: string]: ({ message, port }: FilelessImportPortMessageHandlerParams) => void; - displayLpImportNotification: ({ port }: { port: chrome.runtime.Port }) => void; - startLpImport: ({ message }: { message: FilelessImportPortMessage }) => void; -}; - -interface FilelessImporterBackground { - init(): void; -} - -export { - FilelessImportPortMessage, - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground, -}; diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts deleted file mode 100644 index 7b356b18fd5..00000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { mock } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { - flushPromises, - sendPortMessage, - triggerRuntimeOnConnectEvent, -} from "../../autofill/spec/testing-utils"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; -import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums"; - -import FilelessImporterBackground from "./fileless-importer.background"; - -jest.mock("rxjs", () => { - const rxjs = jest.requireActual("rxjs"); - const { firstValueFrom } = rxjs; - return { - ...rxjs, - firstValueFrom: jest.fn(firstValueFrom), - }; -}); - -describe("FilelessImporterBackground ", () => { - let filelessImporterBackground: FilelessImporterBackground; - const configService = mock(); - const authService = mock(); - const policyService = mock(); - const notificationBackground = mock(); - const importService = mock(); - const syncService = mock(); - const platformUtilsService = mock(); - const logService = mock(); - let scriptInjectorService: BrowserScriptInjectorService; - - beforeEach(() => { - scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); - filelessImporterBackground = new FilelessImporterBackground( - configService, - authService, - policyService, - notificationBackground, - importService, - syncService, - scriptInjectorService, - ); - filelessImporterBackground.init(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("init", () => { - it("sets up the port message listeners on initialization of the class", () => { - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalledWith(expect.any(Function)); - }); - }); - - describe("handle ports onConnect", () => { - let lpImporterPort: chrome.runtime.Port; - let manifestVersionSpy: jest.SpyInstance; - let executeScriptInTabSpy: jest.SpyInstance; - - beforeEach(() => { - lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - executeScriptInTabSpy = jest.spyOn(BrowserApi, "executeScriptInTab").mockResolvedValue(null); - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - jest.spyOn(filelessImporterBackground as any, "removeIndividualVault"); - (firstValueFrom as jest.Mock).mockResolvedValue(false); - }); - - it("ignores the port connection if the port name is not present in the set of filelessImportNames", async () => { - const port = createPortSpyMock("some-other-port"); - - triggerRuntimeOnConnectEvent(port); - await flushPromises(); - - expect(port.postMessage).not.toHaveBeenCalled(); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's auth status is not unlocked", async () => { - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Locked); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the user's policy removes individual vaults", async () => { - (firstValueFrom as jest.Mock).mockResolvedValue(true); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is disabled if the feature flag is turned off", async () => { - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(false); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: false, - }); - }); - - it("posts a message to the port indicating that the fileless import feature is enabled", async () => { - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }); - }); - - it("triggers an injection of the `lp-suppress-import-download.js` script in manifest v3", async () => { - manifestVersionSpy.mockReturnValue(3); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith( - lpImporterPort.sender.tab.id, - { file: "content/lp-suppress-import-download.js", runAt: "document_start", frameId: 0 }, - { world: "MAIN" }, - ); - }); - - it("triggers an injection of the `lp-suppress-import-download-script-append-mv2.js` script in manifest v2", async () => { - manifestVersionSpy.mockReturnValue(2); - - triggerRuntimeOnConnectEvent(lpImporterPort); - await flushPromises(); - - expect(executeScriptInTabSpy).toHaveBeenCalledWith(lpImporterPort.sender.tab.id, { - file: "content/lp-suppress-import-download-script-append-mv2.js", - runAt: "document_start", - frameId: 0, - }); - }); - }); - - describe("port messages", () => { - let notificationPort: chrome.runtime.Port; - let lpImporterPort: chrome.runtime.Port; - - beforeEach(async () => { - jest.spyOn(authService, "getAuthStatus").mockResolvedValue(AuthenticationStatus.Unlocked); - jest.spyOn(configService, "getFeatureFlag").mockResolvedValue(true); - (firstValueFrom as jest.Mock).mockResolvedValue(false); - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.NotificationBar)); - triggerRuntimeOnConnectEvent(createPortSpyMock(FilelessImportPort.LpImporter)); - await flushPromises(); - notificationPort = filelessImporterBackground["importNotificationsPort"]; - lpImporterPort = filelessImporterBackground["lpImporterPort"]; - }); - - it("skips handling a message if a message handler is not associated with the port message command", () => { - sendPortMessage(notificationPort, { command: "commandNotFound" }); - - expect(chrome.tabs.sendMessage).not.toHaveBeenCalled(); - }); - - describe("import notification port messages", () => { - describe("startFilelessImport", () => { - it("sends a message to start the LastPass fileless import within the content script", () => { - sendPortMessage(notificationPort, { - command: "startFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "startLpFilelessImport", - }); - }); - }); - - describe("cancelFilelessImport", () => { - it("sends a message to close the notification bar", async () => { - sendPortMessage(notificationPort, { command: "cancelFilelessImport" }); - - expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( - notificationPort.sender.tab.id, - { - command: "closeNotificationBar", - }, - null, - expect.anything(), - ); - expect(lpImporterPort.postMessage).not.toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - }); - - it("sends a message to trigger a download of the LP importer CSV", () => { - sendPortMessage(notificationPort, { - command: "cancelFilelessImport", - importType: FilelessImportType.LP, - }); - - expect(lpImporterPort.postMessage).toHaveBeenCalledWith({ - command: "triggerCsvDownload", - }); - expect(lpImporterPort.disconnect).toHaveBeenCalled(); - }); - }); - }); - - describe("lp importer port messages", () => { - describe("displayLpImportNotification", () => { - it("creates a request fileless import notification", async () => { - jest.spyOn(filelessImporterBackground["notificationBackground"], "requestFilelessImport"); - - sendPortMessage(lpImporterPort, { - command: "displayLpImportNotification", - }); - await flushPromises(); - - expect( - filelessImporterBackground["notificationBackground"].requestFilelessImport, - ).toHaveBeenCalledWith(lpImporterPort.sender.tab, FilelessImportType.LP); - }); - }); - - describe("startLpImport", () => { - it("ignores the message if the message does not contain data", () => { - sendPortMessage(lpImporterPort, { - command: "startLpImport", - }); - - expect(filelessImporterBackground["importService"].import).not.toHaveBeenCalled(); - }); - - it("triggers the import of the LastPass vault", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest.spyOn(filelessImporterBackground["importService"], "import").mockResolvedValue( - mock({ - success: true, - }), - ); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect(filelessImporterBackground["importService"].import).toHaveBeenCalledWith( - importer, - data, - null, - null, - false, - ); - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportCompleted" }); - expect(filelessImporterBackground["syncService"].fullSync).toHaveBeenCalledWith(true); - }); - - it("posts a failed message if the import fails", async () => { - const data = "url,username,password"; - const importer = mock(); - jest - .spyOn(filelessImporterBackground["importService"], "getImporter") - .mockReturnValue(importer); - jest - .spyOn(filelessImporterBackground["importService"], "import") - .mockImplementation(() => { - throw new Error("error"); - }); - jest.spyOn(filelessImporterBackground["syncService"], "fullSync"); - - sendPortMessage(lpImporterPort, { - command: "startLpImport", - data, - }); - await flushPromises(); - - expect( - filelessImporterBackground["importNotificationsPort"].postMessage, - ).toHaveBeenCalledWith({ command: "filelessImportFailed" }); - }); - }); - }); - }); - - describe("handleImporterPortDisconnect", () => { - it("resets the port properties to null", () => { - const lpImporterPort = createPortSpyMock(FilelessImportPort.LpImporter); - const notificationPort = createPortSpyMock(FilelessImportPort.NotificationBar); - filelessImporterBackground["lpImporterPort"] = lpImporterPort; - filelessImporterBackground["importNotificationsPort"] = notificationPort; - - filelessImporterBackground["handleImporterPortDisconnect"](lpImporterPort); - - expect(filelessImporterBackground["lpImporterPort"]).toBeNull(); - expect(filelessImporterBackground["importNotificationsPort"]).not.toBeNull(); - - filelessImporterBackground["handleImporterPortDisconnect"](notificationPort); - - expect(filelessImporterBackground["importNotificationsPort"]).toBeNull(); - }); - }); -}); diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts deleted file mode 100644 index 21d597ec8ae..00000000000 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ /dev/null @@ -1,265 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom } from "rxjs"; - -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ImportServiceAbstraction } from "@bitwarden/importer/core"; - -import NotificationBackground from "../../autofill/background/notification.background"; -import { BrowserApi } from "../../platform/browser/browser-api"; -import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; -import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts"; -import { - FilelessImportPort, - FilelessImportType, - FilelessImportTypeKeys, -} from "../enums/fileless-import.enums"; - -import { - ImportNotificationMessageHandlers, - LpImporterMessageHandlers, - FilelessImporterBackground as FilelessImporterBackgroundInterface, - FilelessImportPortMessage, -} from "./abstractions/fileless-importer.background"; - -class FilelessImporterBackground implements FilelessImporterBackgroundInterface { - private static readonly filelessImporterPortNames: Set = new Set([ - FilelessImportPort.LpImporter, - FilelessImportPort.NotificationBar, - ]); - private importNotificationsPort: chrome.runtime.Port; - private lpImporterPort: chrome.runtime.Port; - private readonly importNotificationsPortMessageHandlers: ImportNotificationMessageHandlers = { - startFilelessImport: ({ message }) => this.startFilelessImport(message.importType), - cancelFilelessImport: ({ message, port }) => - this.cancelFilelessImport(message.importType, port.sender), - }; - private readonly lpImporterPortMessageHandlers: LpImporterMessageHandlers = { - displayLpImportNotification: ({ port }) => - this.displayFilelessImportNotification(port.sender.tab, FilelessImportType.LP), - startLpImport: ({ message }) => this.triggerLpImport(message.data), - }; - - /** - * Creates a new instance of the fileless importer background logic. - * - * @param configService - Identifies if the feature flag is enabled. - * @param authService - Verifies if the auth status of the user. - * @param policyService - Identifies if the user account has a policy that disables personal ownership. - * @param notificationBackground - Used to inject the notification bar into the tab. - * @param importService - Used to import the export data into the vault. - * @param syncService - Used to trigger a full sync after the import is completed. - * @param scriptInjectorService - Used to inject content scripts that initialize the import process - */ - constructor( - private configService: ConfigService, - private authService: AuthService, - private policyService: PolicyService, - private notificationBackground: NotificationBackground, - private importService: ImportServiceAbstraction, - private syncService: SyncService, - private scriptInjectorService: ScriptInjectorService, - ) {} - - /** - * Initializes the fileless importer background logic. - */ - init() { - this.setupPortMessageListeners(); - } - - /** - * Starts an import of the export data pulled from the tab. - * - * @param importType - The type of import to start. Identifies the used content script. - */ - private startFilelessImport(importType: FilelessImportTypeKeys) { - if (importType === FilelessImportType.LP) { - this.lpImporterPort?.postMessage({ command: "startLpFilelessImport" }); - } - } - - /** - * Cancels an import of the export data pulled from the tab. This closes any - * existing notifications that are present in the tab, and triggers importer - * specific behavior based on the import type. - * - * @param importType - The type of import to cancel. Identifies the used content script. - * @param sender - The sender of the message. - */ - private async cancelFilelessImport( - importType: FilelessImportTypeKeys, - sender: chrome.runtime.MessageSender, - ) { - if (importType === FilelessImportType.LP) { - this.triggerLpImporterCsvDownload(); - } - - await BrowserApi.tabSendMessage(sender.tab, { command: "closeNotificationBar" }); - } - - /** - * Injects the notification bar into the passed tab. - * - * @param tab - * @param importType - */ - private async displayFilelessImportNotification(tab: chrome.tabs.Tab, importType: string) { - await this.notificationBackground.requestFilelessImport(tab, importType); - } - - /** - * Triggers the download of the CSV file from the LP importer. This is triggered - * when the user opts to not save the export to Bitwarden within the notification bar. - */ - private triggerLpImporterCsvDownload() { - this.lpImporterPort?.postMessage({ command: "triggerCsvDownload" }); - this.lpImporterPort?.disconnect(); - } - - /** - * Completes the import process for the LP importer. This is triggered when the - * user opts to save the export to Bitwarden within the notification bar. - * - * @param data - The export data to import. - * @param sender - The sender of the message. - */ - private async triggerLpImport(data: string) { - if (!data) { - return; - } - - const promptForPassword_callback = async () => ""; - const importer = this.importService.getImporter( - "lastpasscsv", - promptForPassword_callback, - null, - ); - - try { - const result = await this.importService.import(importer, data, null, null, false); - if (result.success) { - this.importNotificationsPort?.postMessage({ command: "filelessImportCompleted" }); - await this.syncService.fullSync(true); - } - } catch (error) { - this.importNotificationsPort?.postMessage({ - command: "filelessImportFailed", - importErrorMessage: Object.values(error).length - ? error - : chrome.i18n.getMessage("importNetworkError"), - }); - } - } - - /** - * Identifies if the user account has a policy that disables personal ownership. - */ - private async removeIndividualVault(): Promise { - return await firstValueFrom( - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), - ); - } - - /** - * Sets up onConnect listeners for the extension. - */ - private setupPortMessageListeners() { - chrome.runtime.onConnect.addListener(this.handlePortOnConnect); - } - - /** - * Handles connections from content scripts that affect the fileless importer behavior. - * Is used to facilitate the passing of data and user actions to enact the import - * of web content to the Bitwarden vault. Along with this, a check is made to ensure - * that the feature flag is enabled and the user is authenticated. - */ - private handlePortOnConnect = async (port: chrome.runtime.Port) => { - if (!FilelessImporterBackground.filelessImporterPortNames.has(port.name)) { - return; - } - - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.BrowserFilelessImport, - ); - const userAuthStatus = await this.authService.getAuthStatus(); - const removeIndividualVault = await this.removeIndividualVault(); - const filelessImportEnabled = - filelessImportFeatureFlagEnabled && - userAuthStatus === AuthenticationStatus.Unlocked && - !removeIndividualVault; - port.postMessage({ command: "verifyFeatureFlag", filelessImportEnabled }); - - if (!filelessImportEnabled) { - return; - } - - port.onMessage.addListener(this.handleImporterPortMessage); - port.onDisconnect.addListener(this.handleImporterPortDisconnect); - - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = port; - await this.scriptInjectorService.inject({ - tabId: port.sender.tab.id, - injectDetails: { runAt: "document_start" }, - mv2Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, - mv3Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3, - }); - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = port; - break; - } - }; - - /** - * Handles messages that are sent from fileless importer content scripts. - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handleImporterPortMessage = ( - message: FilelessImportPortMessage, - port: chrome.runtime.Port, - ) => { - let handler: CallableFunction | undefined; - - switch (port.name) { - case FilelessImportPort.LpImporter: - handler = this.lpImporterPortMessageHandlers[message.command]; - break; - case FilelessImportPort.NotificationBar: - handler = this.importNotificationsPortMessageHandlers[message.command]; - break; - } - - if (!handler) { - return; - } - - handler({ message, port }); - }; - - /** - * Handles disconnections from fileless importer content scripts. - * @param port - The port that was disconnected. - */ - private handleImporterPortDisconnect = (port: chrome.runtime.Port) => { - switch (port.name) { - case FilelessImportPort.LpImporter: - this.lpImporterPort = null; - break; - case FilelessImportPort.NotificationBar: - this.importNotificationsPort = null; - break; - } - }; -} - -export default FilelessImporterBackground; diff --git a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts deleted file mode 100644 index 898ee1205ab..00000000000 --- a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - Mv2ScriptInjectionDetails, - Mv3ScriptInjectionDetails, -} from "../../platform/services/abstractions/script-injector.service"; - -type FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: Mv2ScriptInjectionDetails; - mv3: Mv3ScriptInjectionDetails; - }; -}; - -const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConfigurations = { - LpSuppressImportDownload: { - mv2: { - file: "content/lp-suppress-import-download-script-append-mv2.js", - }, - mv3: { - file: "content/lp-suppress-import-download.js", - world: "MAIN", - }, - }, -} as const; - -export { FilelessImporterInjectedScriptsConfig }; diff --git a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts b/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts deleted file mode 100644 index 018ea2c8d94..00000000000 --- a/apps/browser/src/tools/content/abstractions/lp-fileless-importer.ts +++ /dev/null @@ -1,25 +0,0 @@ -type LpFilelessImporterMessage = { - command?: string; - data?: string; - filelessImportEnabled?: boolean; -}; - -type LpFilelessImporterMessageHandlerParams = { - message: LpFilelessImporterMessage; - port: chrome.runtime.Port; -}; - -type LpFilelessImporterMessageHandlers = { - [key: string]: ({ message, port }: LpFilelessImporterMessageHandlerParams) => void; - verifyFeatureFlag: ({ message }: { message: LpFilelessImporterMessage }) => void; - triggerCsvDownload: () => void; - startLpFilelessImport: () => void; -}; - -interface LpFilelessImporter { - init(): void; - handleFeatureFlagVerification(message: LpFilelessImporterMessage): void; - triggerCsvDownload(): void; -} - -export { LpFilelessImporterMessage, LpFilelessImporterMessageHandlers, LpFilelessImporter }; diff --git a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts deleted file mode 100644 index 432754ab91c..00000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { mock } from "jest-mock-extended"; - -import { createPortSpyMock } from "../../autofill/spec/autofill-mocks"; -import { sendPortMessage } from "../../autofill/spec/testing-utils"; -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { LpFilelessImporter } from "./abstractions/lp-fileless-importer"; - -describe("LpFilelessImporter", () => { - let lpFilelessImporter: LpFilelessImporter & { [key: string]: any }; - const portSpy: chrome.runtime.Port = createPortSpyMock(FilelessImportPort.LpImporter); - chrome.runtime.connect = jest.fn(() => portSpy); - - beforeEach(() => { - require("./lp-fileless-importer"); - lpFilelessImporter = (globalThis as any).lpFilelessImporter; - }); - - afterEach(() => { - (globalThis as any).lpFilelessImporter = undefined; - jest.clearAllMocks(); - jest.resetModules(); - Object.defineProperty(document, "readyState", { - value: "complete", - writable: true, - }); - }); - - describe("init", () => { - it("sets up the port connection with the background script", () => { - lpFilelessImporter.init(); - - expect(chrome.runtime.connect).toHaveBeenCalledWith({ - name: FilelessImportPort.LpImporter, - }); - }); - }); - - describe("handleFeatureFlagVerification", () => { - it("disconnects the message port when the fileless import feature is disabled", () => { - lpFilelessImporter.handleFeatureFlagVerification({ filelessImportEnabled: false }); - - expect(portSpy.disconnect).toHaveBeenCalled(); - }); - - it("sets up an event listener for DOMContentLoaded that triggers the importer when the document ready state is `loading`", () => { - Object.defineProperty(document, "readyState", { - value: "loading", - writable: true, - }); - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(document.addEventListener).toHaveBeenCalledWith( - "DOMContentLoaded", - (lpFilelessImporter as any).loadImporter, - ); - }); - - it("sets up a mutation observer to watch the document body for injection of the export content", () => { - const message = { - command: "verifyFeatureFlag", - filelessImportEnabled: true, - }; - jest.spyOn(document, "addEventListener"); - jest.spyOn(window, "MutationObserver").mockImplementationOnce(() => mock()); - - lpFilelessImporter.handleFeatureFlagVerification(message); - - expect(window.MutationObserver).toHaveBeenCalledWith( - (lpFilelessImporter as any).handleMutation, - ); - expect((lpFilelessImporter as any).mutationObserver.observe).toHaveBeenCalledWith( - document.body, - { childList: true, subtree: true }, - ); - }); - }); - - describe("triggerCsvDownload", () => { - it("posts a window message that triggers the download of the LastPass export", () => { - jest.spyOn(globalThis, "postMessage"); - - lpFilelessImporter.triggerCsvDownload(); - - expect(globalThis.postMessage).toHaveBeenCalledWith( - { command: "triggerCsvDownload" }, - "https://lastpass.com", - ); - }); - }); - - describe("handleMutation", () => { - beforeEach(() => { - lpFilelessImporter["mutationObserver"] = mock({ disconnect: jest.fn() }); - jest.spyOn(portSpy, "postMessage"); - }); - - it("ignores mutations that contain empty records", () => { - lpFilelessImporter["handleMutation"]([]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes in the mutation", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations that have no added nodes with a tagname of `pre`", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "div" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain any textContent", () => { - lpFilelessImporter["handleMutation"]([{ addedNodes: [{ nodeName: "pre" }] }]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("ignores mutations where the found `pre` element does not contain the expected header content", () => { - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "some other content" }] }, - ]); - - expect(portSpy.postMessage).not.toHaveBeenCalled(); - }); - - it("will store the export data, display the import notification, and disconnect the mutation observer when the export data is appended", () => { - const observerDisconnectSpy = jest.spyOn( - lpFilelessImporter["mutationObserver"], - "disconnect", - ); - - lpFilelessImporter["handleMutation"]([ - { addedNodes: [{ nodeName: "pre", textContent: "url,username,password" }] }, - ]); - - expect(lpFilelessImporter["exportData"]).toEqual("url,username,password"); - expect(portSpy.postMessage).toHaveBeenCalledWith({ command: "displayLpImportNotification" }); - expect(observerDisconnectSpy).toHaveBeenCalled(); - }); - }); - - describe("handlePortMessage", () => { - it("ignores messages that are not registered with the portMessageHandlers", () => { - const message = { command: "unknownCommand" }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification"); - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).not.toHaveBeenCalled(); - expect(lpFilelessImporter.triggerCsvDownload).not.toHaveBeenCalled(); - }); - - it("handles the port message that verifies the fileless import feature flag", () => { - const message = { command: "verifyFeatureFlag", filelessImportEnabled: true }; - jest.spyOn(lpFilelessImporter, "handleFeatureFlagVerification").mockImplementation(); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.handleFeatureFlagVerification).toHaveBeenCalledWith(message); - }); - - it("handles the port message that triggers the LastPass csv download", () => { - const message = { command: "triggerCsvDownload" }; - jest.spyOn(lpFilelessImporter, "triggerCsvDownload"); - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.triggerCsvDownload).toHaveBeenCalled(); - }); - - describe("handles the port message that triggers the LastPass fileless import", () => { - beforeEach(() => { - jest.spyOn(lpFilelessImporter as any, "postPortMessage"); - }); - - it("skips the import of the export data is not populated", () => { - const message = { command: "startLpFilelessImport" }; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).not.toHaveBeenCalled(); - }); - - it("starts the last pass fileless import", () => { - const message = { command: "startLpFilelessImport" }; - const exportData = "url,username,password"; - lpFilelessImporter["exportData"] = exportData; - - sendPortMessage(portSpy, message); - - expect(lpFilelessImporter.postPortMessage).toHaveBeenCalledWith({ - command: "startLpImport", - data: exportData, - }); - }); - }); - }); -}); diff --git a/apps/browser/src/tools/content/lp-fileless-importer.ts b/apps/browser/src/tools/content/lp-fileless-importer.ts deleted file mode 100644 index 497a499b337..00000000000 --- a/apps/browser/src/tools/content/lp-fileless-importer.ts +++ /dev/null @@ -1,158 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { FilelessImportPort } from "../enums/fileless-import.enums"; - -import { - LpFilelessImporter as LpFilelessImporterInterface, - LpFilelessImporterMessage, - LpFilelessImporterMessageHandlers, -} from "./abstractions/lp-fileless-importer"; - -class LpFilelessImporter implements LpFilelessImporterInterface { - private exportData: string; - private messagePort: chrome.runtime.Port; - private mutationObserver: MutationObserver; - private readonly portMessageHandlers: LpFilelessImporterMessageHandlers = { - verifyFeatureFlag: ({ message }) => this.handleFeatureFlagVerification(message), - triggerCsvDownload: () => this.triggerCsvDownload(), - startLpFilelessImport: () => this.startLpImport(), - }; - - /** - * Initializes the LP fileless importer. - */ - init() { - this.setupMessagePort(); - } - - /** - * Enacts behavior based on the feature flag verification message. If the feature flag is - * not enabled, the message port is disconnected. If the feature flag is enabled, the - * download of the CSV file is suppressed. - * - * @param message - The port message, contains the feature flag indicator. - */ - handleFeatureFlagVerification(message: LpFilelessImporterMessage) { - if (!message.filelessImportEnabled) { - this.messagePort?.disconnect(); - return; - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", this.loadImporter); - return; - } - - this.loadImporter(); - } - - /** - * Posts a message to the LP importer to trigger the download of the CSV file. - */ - triggerCsvDownload() { - this.postWindowMessage({ command: "triggerCsvDownload" }); - } - - /** - * Initializes the importing mechanism used to import the CSV file into Bitwarden. - * This is done by observing the DOM for the addition of the LP importer element. - */ - private loadImporter = () => { - this.mutationObserver = new MutationObserver(this.handleMutation); - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true, - }); - }; - - /** - * Handles mutations that are observed by the mutation observer. When the exported data - * element is added to the DOM, the export data is extracted and the import prompt is - * displayed. - * - * @param mutations - The mutations that were observed. - */ - private handleMutation = (mutations: MutationRecord[]) => { - let textContent: string; - for (let index = 0; index < mutations?.length; index++) { - const mutation: MutationRecord = mutations[index]; - - textContent = Array.from(mutation.addedNodes) - .filter((node) => node.nodeName.toLowerCase() === "pre") - .map((node) => (node as HTMLPreElement).textContent?.trim()) - .find((text) => text?.indexOf("url,username,password") >= 0); - - if (textContent) { - break; - } - } - - if (textContent) { - this.exportData = textContent; - this.postPortMessage({ command: "displayLpImportNotification" }); - this.mutationObserver.disconnect(); - } - }; - - /** - * If the export data is present, sends a message to the background with - * the export data to start the import process. - */ - private startLpImport() { - if (!this.exportData) { - return; - } - - this.postPortMessage({ command: "startLpImport", data: this.exportData }); - this.messagePort?.disconnect(); - } - - /** - * Posts a message to the background script. - * - * @param message - The message to post. - */ - private postPortMessage(message: LpFilelessImporterMessage) { - this.messagePort?.postMessage(message); - } - - /** - * Posts a message to the global context of the page. - * - * @param message - The message to post. - */ - private postWindowMessage(message: LpFilelessImporterMessage) { - globalThis.postMessage(message, "https://lastpass.com"); - } - - /** - * Sets up the message port that is used to facilitate communication between the - * background script and the content script. - */ - private setupMessagePort() { - this.messagePort = chrome.runtime.connect({ name: FilelessImportPort.LpImporter }); - this.messagePort.onMessage.addListener(this.handlePortMessage); - } - - /** - * Handles messages that are sent from the background script. - * - * @param message - The message that was sent. - * @param port - The port that the message was sent from. - */ - private handlePortMessage = (message: LpFilelessImporterMessage, port: chrome.runtime.Port) => { - const handler = this.portMessageHandlers[message.command]; - if (!handler) { - return; - } - - handler({ message, port }); - }; -} - -(function () { - if (!(globalThis as any).lpFilelessImporter) { - (globalThis as any).lpFilelessImporter = new LpFilelessImporter(); - (globalThis as any).lpFilelessImporter.init(); - } -})(); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts deleted file mode 100644 index 95b49ea00ec..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -describe("LP Suppress Import Download for Manifest v2", () => { - it("appends the `lp-suppress-import-download.js` script to the document element", () => { - let createdScriptElement: HTMLScriptElement; - jest.spyOn(window.document, "createElement"); - jest.spyOn(window.document.documentElement, "appendChild").mockImplementation((node) => { - createdScriptElement = node as HTMLScriptElement; - return node; - }); - - require("./lp-suppress-import-download-script-append.mv2"); - - expect(window.document.createElement).toHaveBeenCalledWith("script"); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("content/lp-suppress-import-download.js"); - expect(window.document.documentElement.appendChild).toHaveBeenCalledWith( - expect.any(HTMLScriptElement), - ); - expect(createdScriptElement.src).toBe( - "chrome-extension://id/content/lp-suppress-import-download.js", - ); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts deleted file mode 100644 index cd641590ad1..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * This script handles injection of the LP suppress import download script into the document. - * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. - */ -(function (globalContext) { - const script = globalContext.document.createElement("script"); - script.src = chrome.runtime.getURL("content/lp-suppress-import-download.js"); - globalContext.document.documentElement.appendChild(script); -})(window); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts deleted file mode 100644 index bfff3787506..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { flushPromises, postWindowMessage } from "../../autofill/spec/testing-utils"; - -describe("LP Suppress Import Download", () => { - const downloadAttribute = "file.csv"; - const hrefAttribute = "https://example.com/file.csv"; - const overridenHrefAttribute = "javascript:void(0)"; - let anchor: HTMLAnchorElement; - - beforeEach(() => { - jest.spyOn(Element.prototype, "appendChild"); - jest.spyOn(window, "addEventListener"); - - require("./lp-suppress-import-download"); - - anchor = document.createElement("a"); - anchor.download = downloadAttribute; - anchor.href = hrefAttribute; - anchor.click = jest.fn(); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - it("disables the automatic download anchor", () => { - document.body.appendChild(anchor); - - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - }); - - it("triggers the CSVDownload when receiving a `triggerCsvDownload` window message", async () => { - window.document.createElement = jest.fn(() => anchor); - jest.spyOn(window, "removeEventListener"); - - document.body.appendChild(anchor); - - // Precondition - Ensure the anchor in the document has overridden href and download attributes - expect(anchor.href).toBe(overridenHrefAttribute); - expect(anchor.download).toBe(""); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalled(); - expect(anchor.href).toEqual(hrefAttribute); - expect(anchor.download).toEqual(downloadAttribute); - expect(window.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function)); - }); - - it("skips subsequent calls to trigger a CSVDownload", async () => { - window.document.createElement = jest.fn(() => anchor); - - document.body.appendChild(anchor); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - postWindowMessage({ command: "triggerCsvDownload" }); - await flushPromises(); - - expect(anchor.click).toHaveBeenCalledTimes(1); - }); - - it("skips triggering the CSV download for window messages that do not have the correct command", () => { - document.body.appendChild(anchor); - - postWindowMessage({ command: "notTriggerCsvDownload" }); - - expect(anchor.click).not.toHaveBeenCalled(); - }); - - it("skips triggering the CSV download for window messages that do not have a data value", () => { - document.body.appendChild(anchor); - - postWindowMessage(null); - - expect(anchor.click).not.toHaveBeenCalled(); - }); -}); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.ts b/apps/browser/src/tools/content/lp-suppress-import-download.ts deleted file mode 100644 index 1d5d449d199..00000000000 --- a/apps/browser/src/tools/content/lp-suppress-import-download.ts +++ /dev/null @@ -1,52 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -/** - * Handles intercepting the injection of the CSV download link, and ensures the - * download of the script is suppressed until the user opts to download the file. - * The download is triggered by a window message sent from the LpFilelessImporter - * content script. - */ -(function (globalContext) { - let csvDownload = ""; - let csvHref = ""; - let isCsvDownloadTriggered = false; - const defaultAppendChild = Element.prototype.appendChild; - Element.prototype.appendChild = function (newChild: Node) { - if (isAnchorElement(newChild) && newChild.download) { - csvDownload = newChild.download; - csvHref = newChild.href; - newChild.setAttribute("href", "javascript:void(0)"); - newChild.setAttribute("download", ""); - Element.prototype.appendChild = defaultAppendChild; - } - - return defaultAppendChild.call(this, newChild); - }; - - function isAnchorElement(node: Node): node is HTMLAnchorElement { - return node.nodeName.toLowerCase() === "a"; - } - - const handleWindowMessage = (event: MessageEvent) => { - const command = event.data?.command; - if ( - event.source !== globalContext || - command !== "triggerCsvDownload" || - isCsvDownloadTriggered - ) { - return; - } - - isCsvDownloadTriggered = true; - globalContext.removeEventListener("message", handleWindowMessage); - - const anchor = globalContext.document.createElement("a"); - anchor.setAttribute("href", csvHref); - anchor.setAttribute("download", csvDownload); - globalContext.document.body.appendChild(anchor); - anchor.click(); - globalContext.document.body.removeChild(anchor); - }; - - globalContext.addEventListener("message", handleWindowMessage); -})(window); diff --git a/apps/browser/src/tools/enums/fileless-import.enums.ts b/apps/browser/src/tools/enums/fileless-import.enums.ts deleted file mode 100644 index e20f4f15454..00000000000 --- a/apps/browser/src/tools/enums/fileless-import.enums.ts +++ /dev/null @@ -1,12 +0,0 @@ -const FilelessImportType = { - LP: "LP", -} as const; - -type FilelessImportTypeKeys = (typeof FilelessImportType)[keyof typeof FilelessImportType]; - -const FilelessImportPort = { - NotificationBar: "fileless-importer-notification-bar", - LpImporter: "lp-fileless-importer", -} as const; - -export { FilelessImportType, FilelessImportTypeKeys, FilelessImportPort }; diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index 506d7146dd6..c3f4634a6c2 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -91,7 +91,17 @@ describe("SendV2Component", () => { CurrentAccountComponent, ], providers: [ - { provide: AccountService, useValue: mock() }, + { + provide: AccountService, + useValue: { + activeAccount$: of({ + id: "123", + email: "test@email.com", + emailVerified: true, + name: "Test User", + }), + }, + }, { provide: AuthService, useValue: mock() }, { provide: AvatarService, useValue: mock() }, { diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts index 2d451dddaa7..8b880e88671 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { Observable, firstValueFrom } from "rxjs"; +import { Observable, firstValueFrom, of, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { DialogService, ItemModule } from "@bitwarden/components"; @@ -36,12 +37,19 @@ export class MoreFromBitwardenPageV2Component { constructor( private dialogService: DialogService, - billingAccountProfileStateService: BillingAccountProfileStateService, + private billingAccountProfileStateService: BillingAccountProfileStateService, private environmentService: EnvironmentService, private organizationService: OrganizationService, private familiesPolicyService: FamiliesPolicyService, + private accountService: AccountService, ) { - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + account + ? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), + ); this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$; this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); diff --git a/apps/browser/src/vault/guards/clear-vault-state.guard.ts b/apps/browser/src/vault/guards/clear-vault-state.guard.ts index 2b43f1ecbd3..b212c55d833 100644 --- a/apps/browser/src/vault/guards/clear-vault-state.guard.ts +++ b/apps/browser/src/vault/guards/clear-vault-state.guard.ts @@ -1,7 +1,7 @@ import { inject } from "@angular/core"; import { CanDeactivateFn } from "@angular/router"; -import { VaultV2Component } from "../popup/components/vault/vault-v2.component"; +import { VaultV2Component } from "../popup/components/vault-v2/vault-v2.component"; import { VaultPopupItemsService } from "../popup/services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../popup/services/vault-popup-list-filters.service"; diff --git a/apps/browser/src/vault/popup/components/action-buttons.component.html b/apps/browser/src/vault/popup/components/action-buttons.component.html deleted file mode 100644 index f63c1f1ac32..00000000000 --- a/apps/browser/src/vault/popup/components/action-buttons.component.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/browser/src/vault/popup/components/action-buttons.component.ts b/apps/browser/src/vault/popup/components/action-buttons.component.ts deleted file mode 100644 index fd559aca817..00000000000 --- a/apps/browser/src/vault/popup/components/action-buttons.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; - -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EventType } from "@bitwarden/common/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { PasswordRepromptService } from "@bitwarden/vault"; - -@Component({ - selector: "app-action-buttons", - templateUrl: "action-buttons.component.html", -}) -export class ActionButtonsComponent implements OnInit, OnDestroy { - @Output() onView = new EventEmitter(); - @Output() launchEvent = new EventEmitter(); - @Input() cipher: CipherView; - @Input() showView = false; - - cipherType = CipherType; - userHasPremiumAccess = false; - - private componentIsDestroyed$ = new Subject(); - - constructor( - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private eventCollectionService: EventCollectionService, - private totpService: TotpServiceAbstraction, - private passwordRepromptService: PasswordRepromptService, - private billingAccountProfileStateService: BillingAccountProfileStateService, - ) {} - - ngOnInit() { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.componentIsDestroyed$)) - .subscribe((canAccessPremium: boolean) => { - this.userHasPremiumAccess = canAccessPremium; - }); - } - - ngOnDestroy() { - this.componentIsDestroyed$.next(true); - this.componentIsDestroyed$.complete(); - } - - launchCipher() { - this.launchEvent.emit(this.cipher); - } - - async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) { - if ( - this.cipher.reprompt !== CipherRepromptType.None && - this.passwordRepromptService.protectedFields().includes(aType) && - !(await this.passwordRepromptService.showPasswordPrompt()) - ) { - return; - } - - if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) { - return; - } else if (aType === "TOTP") { - value = await this.totpService.getCode(value); - } - - if (!cipher.viewPassword) { - return; - } - - this.platformUtilsService.copyToClipboard(value, { window: window }); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); - - if (typeI18nKey === "password") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); - } else if (typeI18nKey === "verificationCodeTotp") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, cipher.id); - } else if (typeI18nKey === "securityCode") { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id); - } - } - - displayTotpCopyButton(cipher: CipherView) { - return ( - (cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess) - ); - } - - view() { - this.onView.emit(this.cipher); - } -} diff --git a/apps/browser/src/vault/popup/components/cipher-row.component.html b/apps/browser/src/vault/popup/components/cipher-row.component.html deleted file mode 100644 index 8ac9147cb92..00000000000 --- a/apps/browser/src/vault/popup/components/cipher-row.component.html +++ /dev/null @@ -1,51 +0,0 @@ -
    -
    - - - -
    -
    diff --git a/apps/browser/src/vault/popup/components/cipher-row.component.ts b/apps/browser/src/vault/popup/components/cipher-row.component.ts deleted file mode 100644 index 6b71470a86b..00000000000 --- a/apps/browser/src/vault/popup/components/cipher-row.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, Output } from "@angular/core"; - -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - -@Component({ - selector: "app-cipher-row", - templateUrl: "cipher-row.component.html", -}) -export class CipherRowComponent { - @Output() onSelected = new EventEmitter(); - @Output() launchEvent = new EventEmitter(); - @Output() onView = new EventEmitter(); - @Input() cipher: CipherView; - @Input() last: boolean; - @Input() showView = false; - @Input() title: string; - - selectCipher(c: CipherView) { - this.onSelected.emit(c); - } - - launchCipher(c: CipherView) { - this.launchEvent.emit(c); - } - - viewCipher(c: CipherView) { - this.onView.emit(c); - } -} diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts index 4e222a554f7..cbec7903031 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts @@ -171,7 +171,7 @@ describe("AddEditFolderDialogComponent", () => { it("deletes the folder", async () => { await component.deleteFolder(); - expect(deleteFolder).toHaveBeenCalledWith(folderView.id); + expect(deleteFolder).toHaveBeenCalledWith(folderView.id, ""); expect(showToast).toHaveBeenCalledWith({ variant: "success", title: null, diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts index 5fbd54d9d78..a50403cea2d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts @@ -13,7 +13,7 @@ import { } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -67,6 +67,7 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit { name: ["", Validators.required], }); + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); private destroyRef = inject(DestroyRef); constructor( @@ -114,10 +115,10 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit { this.folder.name = this.folderForm.controls.name.value; try { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$); - const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id); + const activeUserId = await firstValueFrom(this.activeUserId$); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); const folder = await this.folderService.encrypt(this.folder, userKey); - await this.folderApiService.save(folder); + await this.folderApiService.save(folder, activeUserId); this.toastService.showToast({ variant: "success", @@ -144,7 +145,8 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit { } try { - await this.folderApiService.delete(this.folder.id); + const activeUserId = await firstValueFrom(this.activeUserId$); + await this.folderApiService.delete(this.folder.id, activeUserId); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index a2b2d1ce581..2d8c4857c1c 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -326,15 +326,15 @@ export class AddEditV2Component implements OnInit { switch (type) { case CipherType.Login: - return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLocaleLowerCase()); + return this.i18nService.t(partOne, this.i18nService.t("typeLogin")); case CipherType.Card: - return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLocaleLowerCase()); + return this.i18nService.t(partOne, this.i18nService.t("typeCard")); case CipherType.Identity: - return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase()); + return this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); case CipherType.SecureNote: - return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase()); + return this.i18nService.t(partOne, this.i18nService.t("note")); case CipherType.SshKey: - return this.i18nService.t(partOne, this.i18nService.t("typeSshKey").toLocaleLowerCase()); + return this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 8c1e0641b03..4f6c4aa07cf 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -10,7 +10,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -55,7 +54,14 @@ describe("OpenAttachmentsComponent", () => { const showFilePopoutMessage = jest.fn().mockReturnValue(false); const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const accountService = { + activeAccount$: of({ + id: mockUserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }), + }; beforeEach(async () => { openCurrentPagePopout.mockClear(); @@ -63,6 +69,7 @@ describe("OpenAttachmentsComponent", () => { showToast.mockClear(); getOrganization.mockClear(); showFilePopoutMessage.mockClear(); + hasPremiumFromAnySource$.next(true); await TestBed.configureTestingModule({ imports: [OpenAttachmentsComponent, RouterTestingModule], @@ -96,7 +103,7 @@ describe("OpenAttachmentsComponent", () => { }).compileComponents(); }); - beforeEach(() => { + beforeEach(async () => { fixture = TestBed.createComponent(OpenAttachmentsComponent); component = fixture.componentInstance; component.cipherId = "5555-444-3333" as CipherId; @@ -107,7 +114,7 @@ describe("OpenAttachmentsComponent", () => { it("opens attachments in new popout", async () => { showFilePopoutMessage.mockReturnValue(true); - + component.canAccessAttachments = true; await component.ngOnInit(); await component.openAttachments(); @@ -120,7 +127,7 @@ describe("OpenAttachmentsComponent", () => { it("opens attachments in same window", async () => { showFilePopoutMessage.mockReturnValue(false); - + component.canAccessAttachments = true; await component.ngOnInit(); await component.openAttachments(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index ca620531ca8..5e27ccd5c41 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -54,8 +54,13 @@ export class OpenAttachmentsComponent implements OnInit { private filePopoutUtilsService: FilePopoutUtilsService, private accountService: AccountService, ) { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntilDestroyed()) + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntilDestroyed(), + ) .subscribe((canAccessPremium) => { this.canAccessAttachments = canAccessPremium; }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index 52ae387e8b9..eae8e2cc980 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -1,9 +1,10 @@ diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index 8e72d84053d..d2dd21be6d8 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -1,8 +1,9 @@ import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { combineLatest, map, Observable } from "rxjs"; +import { Component, OnInit } from "@angular/core"; +import { combineLatest, firstValueFrom, map, Observable } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { IconButtonModule, @@ -31,7 +32,7 @@ import { VaultListItemsContainerComponent } from "../vault-list-items-container/ selector: "app-autofill-vault-list-items", templateUrl: "autofill-vault-list-items.component.html", }) -export class AutofillVaultListItemsComponent { +export class AutofillVaultListItemsComponent implements OnInit { /** * The list of ciphers that can be used to autofill the current page. * @protected @@ -45,6 +46,8 @@ export class AutofillVaultListItemsComponent { */ protected showRefresh: boolean = BrowserPopupUtils.inSidebar(window); + clickItemsToAutofillVaultView = false; + /** * Observable that determines whether the empty autofill tip should be shown. * The tip is shown when there are no login ciphers to autofill, no filter is applied, and autofill is allowed in @@ -62,13 +65,26 @@ export class AutofillVaultListItemsComponent { ), ); + /** + * Flag indicating that the current tab location is blocked + */ + currentURIIsBlocked$: Observable = + this.vaultPopupAutofillService.currentTabIsOnBlocklist$; + constructor( private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupAutofillService: VaultPopupAutofillService, + private vaultSettingsService: VaultSettingsService, ) { // TODO: Migrate logic to show Autofill policy toast PM-8144 } + async ngOnInit() { + this.clickItemsToAutofillVaultView = await firstValueFrom( + this.vaultSettingsService.clickItemsToAutofillVaultView$, + ); + } + /** * Refreshes the current tab to re-populate the autofill ciphers. * @protected diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html new file mode 100644 index 00000000000..05db600bd5a --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.html @@ -0,0 +1,10 @@ + + {{ "autofillBlockedNoticeV2" | i18n }} + + {{ "autofillBlockedNoticeGuidance" | i18n }} + + 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 new file mode 100644 index 00000000000..3a17825f4fb --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts @@ -0,0 +1,53 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + BannerModule, + IconButtonModule, + LinkModule, + TypographyModule, +} from "@bitwarden/components"; + +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; + +const blockedURISettingsRoute = "/blocked-domains"; + +@Component({ + standalone: true, + imports: [ + BannerModule, + CommonModule, + IconButtonModule, + JslibModule, + LinkModule, + RouterModule, + TypographyModule, + ], + selector: "blocked-injection-banner", + templateUrl: "blocked-injection-banner.component.html", +}) +export class BlockedInjectionBanner implements OnInit { + /** + * Flag indicating that the banner should be shown + */ + protected showCurrentTabIsBlockedBanner$: Observable = + this.vaultPopupAutofillService.showCurrentTabIsBlockedBanner$; + + /** + * Hostname for current tab + */ + protected currentTabHostname?: string; + + blockedURISettingsRoute: string = blockedURISettingsRoute; + + constructor(private vaultPopupAutofillService: VaultPopupAutofillService) {} + + async ngOnInit() {} + + async handleCurrentTabIsBlockedBannerDismiss() { + await this.vaultPopupAutofillService.dismissCurrentTabIsBlockedBanner(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index 8b70896e019..4c7067df53a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -5,6 +5,7 @@ size="small" [attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name" [title]="'moreOptionsTitle' | i18n: cipher.name" + [disabled]="cipher.decryptionFailure" [bitMenuTriggerFor]="moreOptions" > @@ -18,6 +19,11 @@
    + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index 9dca0eec7c2..5d3dee9018e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -46,6 +46,13 @@ export class ItemMoreOptionsComponent implements OnInit { return this._cipher$.value; } + /** + * Flag to show view item menu option. Used when something else is + * assigned as the primary action for the item, such as autofill. + */ + @Input({ transform: booleanAttribute }) + showViewOption: boolean; + /** * Flag to hide the autofill menu options. Used for items that are * already in the autofill list suggestion. @@ -111,6 +118,16 @@ export class ItemMoreOptionsComponent implements OnInit { await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, false); } + async onView() { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(this.cipher); + if (!repromptPassed) { + return; + } + await this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: this.cipher.id, type: this.cipher.type }, + }); + } + /** * Toggles the favorite status of the cipher and updates it on the server. */ diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html index 7b31e647bf0..4d617ff7786 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html @@ -27,6 +27,15 @@ {{ "note" | i18n }} + + + {{ "typeSshKey" | i18n }} + - {{ ciphers.length }} - - + + + + + + + + +
    + +
    + + +
    + + + + +

    + {{ title }} +

    + + + + {{ ciphers.length }} + + + + + +
    +
    + +
    {{ description }}
    +
    + + - +
    + +
    {{ cipher.name }} {{ cipher.subTitle }} - + - - - - `, - imports: [CommonModule, DialogModule, ButtonModule, JslibModule, IconModule], -}) -export class VaultUiOnboardingComponent { - icon = announcementIcon; - - static open(dialogService: DialogService) { - return dialogService.open(VaultUiOnboardingComponent); - } - - navigateToLink = async () => { - window.open( - "https://bitwarden.com/blog/bringing-intuitive-workflows-and-visual-updates-to-the-bitwarden-browser/", - "_blank", - ); - }; -} diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html similarity index 93% rename from apps/browser/src/vault/popup/components/vault/vault-v2.component.html rename to apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 4798ddf4dfb..43471e56e7b 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -22,6 +22,11 @@ + + diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts similarity index 73% rename from apps/browser/src/vault/popup/components/vault/vault-v2.component.ts rename to apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 68aa40cbf62..7c21c7e6a0c 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -1,15 +1,17 @@ import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, DestroyRef, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { RouterLink } from "@angular/router"; import { combineLatest, Observable, shareReplay, switchMap } from "rxjs"; +import { filter, map, take } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; -import { VaultIcons } from "@bitwarden/vault"; +import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components"; +import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault"; import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component"; @@ -17,13 +19,15 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; -import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service"; -import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2"; + +import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component"; import { NewItemDropdownV2Component, NewItemInitialValues, -} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component"; -import { VaultHeaderV2Component } from "../vault-v2/vault-header/vault-header-v2.component"; +} from "./new-item-dropdown/new-item-dropdown-v2.component"; +import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; + +import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; enum VaultState { Empty, @@ -36,6 +40,7 @@ enum VaultState { templateUrl: "vault-v2.component.html", standalone: true, imports: [ + BlockedInjectionBanner, PopupPageComponent, PopupHeaderComponent, PopOutComponent, @@ -50,8 +55,8 @@ enum VaultState { NewItemDropdownV2Component, ScrollingModule, VaultHeaderV2Component, + DecryptionFailureDialogComponent, ], - providers: [VaultUiOnboardingService], }) export class VaultV2Component implements OnInit, OnDestroy { cipherType = CipherType; @@ -86,7 +91,9 @@ export class VaultV2Component implements OnInit, OnDestroy { constructor( private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupListFiltersService: VaultPopupListFiltersService, - private vaultUiOnboardingService: VaultUiOnboardingService, + private destroyRef: DestroyRef, + private cipherService: CipherService, + private dialogService: DialogService, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, @@ -113,7 +120,18 @@ export class VaultV2Component implements OnInit, OnDestroy { } async ngOnInit() { - await this.vaultUiOnboardingService.showOnboardingDialog(); + this.cipherService.failedToDecryptCiphers$ + .pipe( + map((ciphers) => ciphers.filter((c) => !c.isDeleted)), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); } ngOnDestroy(): void {} diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index bc84dd337cb..526ab2e2579 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -137,21 +137,21 @@ describe("ViewV2Component", () => { params$.next({ cipherId: mockCipher.id }); flush(); // Resolve all promises - expect(component.headerText).toEqual("viewItemHeader typelogin"); + expect(component.headerText).toEqual("viewItemHeader typeLogin"); // Set header text for a card mockCipher.type = CipherType.Card; params$.next({ cipherId: mockCipher.id }); flush(); // Resolve all promises - expect(component.headerText).toEqual("viewItemHeader typecard"); + expect(component.headerText).toEqual("viewItemHeader typeCard"); // Set header text for an identity mockCipher.type = CipherType.Identity; params$.next({ cipherId: mockCipher.id }); flush(); // Resolve all promises - expect(component.headerText).toEqual("viewItemHeader typeidentity"); + expect(component.headerText).toEqual("viewItemHeader typeIdentity"); // Set header text for a secure note mockCipher.type = CipherType.SecureNote; @@ -179,7 +179,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(doAutofill).toHaveBeenCalledOnce(); + expect(doAutofill).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-username"', fakeAsync(() => { @@ -187,7 +187,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-password"', fakeAsync(() => { @@ -195,7 +195,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it('invokes `copy` when action="copy-totp"', fakeAsync(() => { @@ -203,7 +203,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(copy).toHaveBeenCalledOnce(); + expect(copy).toHaveBeenCalledTimes(1); })); it("closes the popout after a load action", fakeAsync(() => { @@ -218,9 +218,9 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(doAutofill).toHaveBeenCalledOnce(); + expect(doAutofill).toHaveBeenCalledTimes(1); expect(focusSpy).toHaveBeenCalledWith(99); - expect(closeSpy).toHaveBeenCalledOnce(); + expect(closeSpy).toHaveBeenCalledTimes(1); })); }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 5afbf8ba77d..f3cd713dd5f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -37,7 +37,11 @@ import { } from "@bitwarden/components"; import { CopyCipherFieldService } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PremiumUpgradePromptService } from "../../../../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; @@ -146,18 +150,15 @@ export class ViewV2Component { setHeader(type: CipherType) { switch (type) { case CipherType.Login: - return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin").toLowerCase()); + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin")); case CipherType.Card: - return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard").toLowerCase()); + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard")); case CipherType.Identity: - return this.i18nService.t( - "viewItemHeader", - this.i18nService.t("typeIdentity").toLowerCase(), - ); + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeIdentity")); case CipherType.SecureNote: - return this.i18nService.t("viewItemHeader", this.i18nService.t("note").toLowerCase()); + return this.i18nService.t("viewItemHeader", this.i18nService.t("note")); case CipherType.SshKey: - return this.i18nService.t("viewItemHeader", this.i18nService.t("typeSshkey").toLowerCase()); + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeSshkey")); } } @@ -234,7 +235,11 @@ export class ViewV2Component { } protected showFooter(): boolean { - return this.cipher && (!this.cipher.isDeleted || (this.cipher.isDeleted && this.cipher.edit)); + return ( + this.cipher && + (!this.cipher.isDeleted || + (this.cipher.isDeleted && this.cipher.edit && this.cipher.viewPassword)) + ); } /** diff --git a/apps/browser/src/vault/popup/components/vault/add-edit-custom-fields.component.html b/apps/browser/src/vault/popup/components/vault/add-edit-custom-fields.component.html deleted file mode 100644 index 8464655c20b..00000000000 --- a/apps/browser/src/vault/popup/components/vault/add-edit-custom-fields.component.html +++ /dev/null @@ -1,140 +0,0 @@ -
    -

    - {{ "customFields" | i18n }} -

    -
    - -
    -
    - - - -
    - - - - - - - -
    - - -
    - -
    -
    - -
    -
    -
    - -
    - - - -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/add-edit-custom-fields.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit-custom-fields.component.ts deleted file mode 100644 index 6992455a8a6..00000000000 --- a/apps/browser/src/vault/popup/components/vault/add-edit-custom-fields.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component } from "@angular/core"; - -import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/vault/components/add-edit-custom-fields.component"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; - -@Component({ - selector: "app-vault-add-edit-custom-fields", - templateUrl: "add-edit-custom-fields.component.html", -}) -export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent { - constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) { - super(i18nService, eventCollectionService); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html deleted file mode 100644 index fb1efbbbd79..00000000000 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ /dev/null @@ -1,826 +0,0 @@ -
    -
    -
    - -
    -

    - {{ title }} -

    -
    - -
    -
    -
    - - {{ "personalOwnershipPolicyInEffect" | i18n }} - -
    -

    - {{ "itemInformation" | i18n }} -

    -
    -
    - - -
    -
    - - -
    - -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - - - -
    -
    - - -
    -
    -
    - -
    - {{ "typePasskey" | i18n }} - {{ "dateCreated" | i18n }} - {{ cipher.login.fido2Credentials[0].creationDate | date: "short" }} -
    -
    -
    -
    - -
    -
    - - -
    -
    - - - -
    -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    - -
    -
    -
    - - - - - - - -
    -
    - - - - - - - -
    -
    - - -
    -
    -
    - - -
    -
    - -
    -
    -
    - -
    -
    - - - - - - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - {{ "sshPrivateKey" | i18n }} - {{ cipher.sshKey.privateKey }} -
    -
    - {{ "sshPublicKey" | i18n }} - {{ cipher.sshKey.publicKey }} -
    -
    - {{ "sshKeyFingerprint" | i18n }} - {{ cipher.sshKey.keyFingerprint }} -
    -
    -
    -
    -
    -
    - -
    - -
    - - - - - - -
    -
    - - -
    -
    -
    - -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    - - -
    -
    -
    -

    - -

    -
    -
    - -
    -
    -
    - - -
    -

    - {{ "ownership" | i18n }} -

    -
    -
    - - -
    -
    -
    -
    -

    - {{ "collections" | i18n }} -

    -
    -
    - {{ "noCollectionsInList" | i18n }} -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts deleted file mode 100644 index 39414217b0d..00000000000 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ /dev/null @@ -1,417 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DatePipe, Location } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import qrcodeParser from "qrcode-parser"; -import { firstValueFrom } from "rxjs"; -import { first } from "rxjs/operators"; - -import { CollectionService } from "@bitwarden/admin-console/common"; -import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; - -import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service"; -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service"; -import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service"; -import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data"; -import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window"; - -@Component({ - selector: "app-vault-add-edit", - templateUrl: "add-edit.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class AddEditComponent extends BaseAddEditComponent implements OnInit { - currentUris: string[]; - showAttachments = true; - openAttachmentsInPopup: boolean; - showAutoFillOnPageLoadOptions: boolean; - - private fido2PopoutSessionData$ = fido2PopoutSessionData$(); - - constructor( - cipherService: CipherService, - folderService: FolderService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - auditService: AuditService, - accountService: AccountService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - collectionService: CollectionService, - messagingService: MessagingService, - private route: ActivatedRoute, - private router: Router, - private location: Location, - eventCollectionService: EventCollectionService, - policyService: PolicyService, - private popupCloseWarningService: PopupCloseWarningService, - organizationService: OrganizationService, - passwordRepromptService: PasswordRepromptService, - logService: LogService, - dialogService: DialogService, - datePipe: DatePipe, - configService: ConfigService, - private fido2UserVerificationService: Fido2UserVerificationService, - cipherAuthorizationService: CipherAuthorizationService, - ) { - super( - cipherService, - folderService, - i18nService, - platformUtilsService, - auditService, - accountService, - collectionService, - messagingService, - eventCollectionService, - policyService, - logService, - passwordRepromptService, - organizationService, - dialogService, - window, - datePipe, - configService, - cipherAuthorizationService, - ); - } - - async ngOnInit() { - await super.ngOnInit(); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - if (params.cipherId) { - this.cipherId = params.cipherId; - } - if (params.folderId) { - this.folderId = params.folderId; - } - if (params.collectionId) { - this.collectionId = params.collectionId; - const collection = this.writeableCollections.find((c) => c.id === params.collectionId); - if (collection != null) { - this.collectionIds = [collection.id]; - this.organizationId = collection.organizationId; - } - } - if (params.type) { - const type = parseInt(params.type, null); - this.type = type; - } - this.editMode = !params.cipherId; - - if (params.cloneMode != null) { - this.cloneMode = params.cloneMode === "true"; - } - if (params.selectedVault) { - this.organizationId = params.selectedVault; - } - - await this.load(); - - if (!this.editMode || this.cloneMode) { - // Only allow setting username if there's no existing value - if ( - params.username && - (this.cipher.login.username == null || this.cipher.login.username === "") - ) { - this.cipher.login.username = params.username; - } - - if (params.name && (this.cipher.name == null || this.cipher.name === "")) { - this.cipher.name = params.name; - } - if ( - params.uri && - this.cipher.login.uris[0] && - (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "") - ) { - this.cipher.login.uris[0].uri = params.uri; - } - } - - this.openAttachmentsInPopup = BrowserPopupUtils.inPopup(window); - - if (this.inAddEditPopoutWindow()) { - BrowserApi.messageListener("add-edit-popout", this.handleExtensionMessage.bind(this)); - } - }); - - if (!this.editMode) { - const tabs = await BrowserApi.tabsQuery({ windowType: "normal" }); - this.currentUris = - tabs == null - ? null - : tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url); - } - - this.setFocus(); - - if (BrowserPopupUtils.inPopout(window)) { - this.popupCloseWarningService.enable(); - } - } - - async load() { - await super.load(); - this.showAutoFillOnPageLoadOptions = - this.cipher.type === CipherType.Login && - (await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)); - } - - async submit(): Promise { - const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$); - const { isFido2Session, sessionId, userVerification } = fido2SessionData; - const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session; - - // normalize card expiry year on save - if (this.cipher.type === this.cipherType.Card) { - this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear); - } - - // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - if ( - inFido2PopoutWindow && - !(await this.handleFido2UserVerification(sessionId, userVerification)) - ) { - return false; - } - - const success = await super.submit(); - if (!success) { - return false; - } - - if (BrowserPopupUtils.inPopout(window)) { - this.popupCloseWarningService.disable(); - } - - if (inFido2PopoutWindow) { - BrowserFido2UserInterfaceSession.confirmNewCredentialResponse( - sessionId, - this.cipher.id, - userVerification, - ); - return true; - } - - if (this.inAddEditPopoutWindow()) { - this.messagingService.send("addEditCipherSubmitted"); - await closeAddEditVaultItemPopout(1000); - return true; - } - - if (this.cloneMode) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/tabs/vault"]); - } else { - this.location.back(); - } - return true; - } - - attachments() { - super.attachments(); - - if (this.openAttachmentsInPopup) { - const destinationUrl = this.router - .createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } }) - .toString(); - const currentBaseUrl = window.location.href.replace(this.router.url, ""); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } }); - } - } - - editCollections() { - super.editCollections(); - if (this.cipher.organizationId != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/collections"], { queryParams: { cipherId: this.cipher.id } }); - } - } - - async cancel() { - super.cancel(); - - const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); - if (BrowserPopupUtils.inPopout(window) && sessionData.isFido2Session) { - this.popupCloseWarningService.disable(); - BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId); - return; - } - - if (this.inAddEditPopoutWindow()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - closeAddEditVaultItemPopout(); - return; - } - - this.location.back(); - } - - async generateUsername(): Promise { - const confirmed = await super.generateUsername(); - if (confirmed) { - await this.saveCipherState(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["generator"], { queryParams: { type: "username" } }); - } - return confirmed; - } - - async generatePassword(): Promise { - const confirmed = await super.generatePassword(); - if (confirmed) { - await this.saveCipherState(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["generator"], { queryParams: { type: "password" } }); - } - return confirmed; - } - - async delete(): Promise { - const confirmed = await super.delete(); - if (confirmed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/tabs/vault"]); - } - return confirmed; - } - - toggleUriInput(uri: LoginUriView) { - const u = uri as any; - u.showCurrentUris = !u.showCurrentUris; - } - - allowOwnershipOptions(): boolean { - return ( - (!this.editMode || this.cloneMode) && - this.ownershipOptions && - (this.ownershipOptions.length > 1 || !this.allowPersonal) - ); - } - - private saveCipherState() { - return this.cipherService.setAddEditCipherInfo({ - cipher: this.cipher, - collectionIds: - this.collections == null - ? [] - : this.collections.filter((c) => (c as any).checked).map((c) => c.id), - }); - } - - private setFocus() { - window.setTimeout(() => { - if (this.editMode) { - return; - } - - if (this.cipher.name != null && this.cipher.name !== "") { - document.getElementById("loginUsername").focus(); - } else { - document.getElementById("name").focus(); - } - }, 200); - } - - repromptChanged() { - super.repromptChanged(); - - if (!this.showAutoFillOnPageLoadOptions) { - return; - } - - if (this.reprompt) { - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("passwordRepromptDisabledAutofillOnPageLoad"), - ); - return; - } - - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("autofillOnPageLoadSetToDefault"), - ); - } - - private inAddEditPopoutWindow() { - return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem); - } - - async captureTOTPFromTab() { - try { - const screenshot = await BrowserApi.captureVisibleTab(); - const data = await qrcodeParser(screenshot); - const url = new URL(data.toString()); - if (url.protocol == "otpauth:" && url.searchParams.has("secret")) { - this.cipher.login.totp = data.toString(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("totpCaptureSuccess"), - ); - } - } catch (e) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("totpCaptureError"), - ); - } - } - - private handleExtensionMessage(message: { [key: string]: any; command: string }) { - if (message.command === "inlineAutofillMenuRefreshAddEditCipher") { - this.load().catch((error) => this.logService.error(error)); - } - } - - // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. - // Be sure to make the same changes to add-edit-v2.component.ts if applicable - private async handleFido2UserVerification( - sessionId: string, - userVerification: boolean, - ): Promise { - // We are bypassing user verification pending approval for production. - return true; - } -} diff --git a/apps/browser/src/vault/popup/components/vault/attachments.component.html b/apps/browser/src/vault/popup/components/vault/attachments.component.html deleted file mode 100644 index b95dc69af8f..00000000000 --- a/apps/browser/src/vault/popup/components/vault/attachments.component.html +++ /dev/null @@ -1,72 +0,0 @@ -
    -
    -
    - - -
    -

    - {{ "attachments" | i18n }} -

    -
    - -
    -
    -
    -
    -
    -
    -
    - {{ a.fileName }} -
    - {{ a.sizeName }} -
    - -
    -
    -
    -
    -
    -

    - {{ "newAttachment" | i18n }} -

    -
    -
    - - -
    -
    - -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/attachments.component.ts b/apps/browser/src/vault/popup/components/vault/attachments.component.ts deleted file mode 100644 index b63b743b9fa..00000000000 --- a/apps/browser/src/vault/popup/components/vault/attachments.component.ts +++ /dev/null @@ -1,82 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Location } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService, ToastService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -@Component({ - selector: "app-vault-attachments", - templateUrl: "attachments.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit { - openedAttachmentsInPopup: boolean; - - constructor( - cipherService: CipherService, - i18nService: I18nService, - keyService: KeyService, - encryptService: EncryptService, - platformUtilsService: PlatformUtilsService, - apiService: ApiService, - private location: Location, - private route: ActivatedRoute, - stateService: StateService, - logService: LogService, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - billingAccountProfileStateService: BillingAccountProfileStateService, - accountService: AccountService, - toastService: ToastService, - ) { - super( - cipherService, - i18nService, - keyService, - encryptService, - platformUtilsService, - apiService, - window, - logService, - stateService, - fileDownloadService, - dialogService, - billingAccountProfileStateService, - accountService, - toastService, - ); - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - this.cipherId = params.cipherId; - await this.init(); - }); - - this.openedAttachmentsInPopup = history.length === 1; - } - - back() { - this.location.back(); - } - - close() { - window.close(); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.html b/apps/browser/src/vault/popup/components/vault/collections.component.html deleted file mode 100644 index 36c1336c5b4..00000000000 --- a/apps/browser/src/vault/popup/components/vault/collections.component.html +++ /dev/null @@ -1,43 +0,0 @@ -
    -
    -
    - -
    -

    - {{ "collections" | i18n }} -

    -
    - -
    -
    -
    -
    -
    -
    - {{ "noCollectionsInList" | i18n }} -
    -
    -
    -
    - - -
    -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.ts b/apps/browser/src/vault/popup/components/vault/collections.component.ts deleted file mode 100644 index 407f87e996c..00000000000 --- a/apps/browser/src/vault/popup/components/vault/collections.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Location } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { CollectionService } from "@bitwarden/admin-console/common"; -import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { ToastService } from "@bitwarden/components"; - -@Component({ - selector: "app-vault-collections", - templateUrl: "collections.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class CollectionsComponent extends BaseCollectionsComponent implements OnInit { - constructor( - collectionService: CollectionService, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - cipherService: CipherService, - organizationService: OrganizationService, - private route: ActivatedRoute, - private location: Location, - logService: LogService, - accountService: AccountService, - toastService: ToastService, - ) { - super( - collectionService, - platformUtilsService, - i18nService, - cipherService, - organizationService, - logService, - accountService, - toastService, - ); - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.onSavedCollections.subscribe(() => { - this.back(); - }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - this.cipherId = params.cipherId; - await this.load(); - }); - } - - back() { - this.location.back(); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html deleted file mode 100644 index bb8a401da62..00000000000 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.html +++ /dev/null @@ -1,95 +0,0 @@ - -

    {{ "currentTab" | i18n }}

    -
    - - -
    - -
    - -
    -
    -
    -
    - -
    - - -
    -

    - {{ "typeLogins" | i18n }} - {{ loginCiphers.length }} -

    -
    - - -
    -

    {{ "autoFillInfo" | i18n }}

    - -
    -
    -
    -
    -

    - {{ "cards" | i18n }} - {{ cardCiphers.length }} -

    -
    - -
    -
    -
    -

    - {{ "identities" | i18n }} - {{ identityCiphers.length }} -

    -
    - -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts deleted file mode 100644 index 156a708015f..00000000000 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ /dev/null @@ -1,354 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { Subject, firstValueFrom, from, Subscription } from "rxjs"; -import { debounceTime, switchMap, takeUntil } from "rxjs/operators"; - -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { PasswordRepromptService } from "@bitwarden/vault"; - -import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service"; -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { VaultFilterService } from "../../../services/vault-filter.service"; - -const BroadcasterSubscriptionId = "CurrentTabComponent"; - -@Component({ - selector: "app-current-tab", - templateUrl: "current-tab.component.html", -}) -export class CurrentTabComponent implements OnInit, OnDestroy { - pageDetails: any[] = []; - tab: chrome.tabs.Tab; - cardCiphers: CipherView[]; - identityCiphers: CipherView[]; - loginCiphers: CipherView[]; - url: string; - hostname: string; - searchText: string; - inSidebar = false; - searchTypeSearch = false; - loaded = false; - isLoading = false; - showOrganizations = false; - showHowToAutofill = false; - autofillCalloutText: string; - protected search$ = new Subject(); - private destroy$ = new Subject(); - private collectPageDetailsSubscription: Subscription; - - private totpCode: string; - private totpTimeout: number; - private loadedTimeout: number; - private searchTimeout: number; - - constructor( - private platformUtilsService: PlatformUtilsService, - private cipherService: CipherService, - private autofillService: AutofillService, - private i18nService: I18nService, - private router: Router, - private ngZone: NgZone, - private broadcasterService: BroadcasterService, - private changeDetectorRef: ChangeDetectorRef, - private syncService: SyncService, - private searchService: SearchService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private passwordRepromptService: PasswordRepromptService, - private organizationService: OrganizationService, - private vaultFilterService: VaultFilterService, - private vaultSettingsService: VaultSettingsService, - ) {} - - async ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - this.inSidebar = BrowserPopupUtils.inSidebar(window); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - if (this.isLoading) { - window.setTimeout(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - }, 500); - } - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - - if (!this.syncService.syncInProgress) { - await this.load(); - await this.setCallout(); - } else { - this.loadedTimeout = window.setTimeout(async () => { - if (!this.isLoading) { - await this.load(); - await this.setCallout(); - } - }, 5000); - } - - this.search$ - .pipe( - debounceTime(500), - switchMap(() => { - return from(this.searchVault()); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - const autofillOnPageLoadOrgPolicy = await firstValueFrom( - this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$, - ); - const autofillOnPageLoadPolicyToastHasDisplayed = await firstValueFrom( - this.autofillSettingsService.autofillOnPageLoadPolicyToastHasDisplayed$, - ); - - // If the org "autofill on page load" policy is set, set the user setting to match it - // @TODO override user setting instead of overwriting - if (autofillOnPageLoadOrgPolicy === true) { - await this.autofillSettingsService.setAutofillOnPageLoad(true); - - if (!autofillOnPageLoadPolicyToastHasDisplayed) { - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("autofillPageLoadPolicyActivated"), - ); - - await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(true); - } - } - - // If the org policy is ever disabled after being enabled, reset the toast notification - if (!autofillOnPageLoadOrgPolicy && autofillOnPageLoadPolicyToastHasDisplayed) { - await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(false); - } - } - - ngOnDestroy() { - window.clearTimeout(this.loadedTimeout); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - - this.destroy$.next(); - this.destroy$.complete(); - } - - async refresh() { - await this.load(); - } - - addCipher() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-cipher"], { - queryParams: { - name: this.hostname, - uri: this.url, - selectedVault: this.vaultFilterService.getVaultFilter().selectedOrganizationId, - }, - }); - } - - viewCipher(cipher: CipherView) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); - } - - async fillCipher(cipher: CipherView, closePopupDelay?: number) { - if ( - cipher.reprompt !== CipherRepromptType.None && - !(await this.passwordRepromptService.showPasswordPrompt()) - ) { - return; - } - - this.totpCode = null; - if (this.totpTimeout != null) { - window.clearTimeout(this.totpTimeout); - } - - if (this.pageDetails == null || this.pageDetails.length === 0) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); - return; - } - - try { - this.totpCode = await this.autofillService.doAutoFill({ - tab: this.tab, - cipher: cipher, - pageDetails: this.pageDetails, - doc: window.document, - fillNewPassword: true, - allowTotpAutofill: true, - }); - if (this.totpCode != null) { - this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); - } - if (BrowserPopupUtils.inPopup(window)) { - if (!closePopupDelay) { - if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) { - BrowserApi.closePopup(window); - } else { - // Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard - setTimeout(() => BrowserApi.closePopup(window), 50); - } - } else { - setTimeout(() => BrowserApi.closePopup(window), closePopupDelay); - } - } - } catch { - this.ngZone.run(() => { - this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); - this.changeDetectorRef.detectChanges(); - }); - } - } - - async searchVault() { - if (!(await this.searchService.isSearchable(this.searchText))) { - return; - } - - await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } }); - } - - closeOnEsc(e: KeyboardEvent) { - // If input not empty, use browser default behavior of clearing input instead - if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) { - BrowserApi.closePopup(window); - } - } - - protected async load() { - this.isLoading = false; - this.tab = await BrowserApi.getTabFromCurrentWindow(); - - if (this.tab != null) { - this.url = this.tab.url; - } else { - this.loginCiphers = []; - this.isLoading = this.loaded = true; - return; - } - - this.pageDetails = []; - this.collectPageDetailsSubscription?.unsubscribe(); - this.collectPageDetailsSubscription = this.autofillService - .collectPageDetailsFromTab$(this.tab) - .pipe(takeUntil(this.destroy$)) - .subscribe((pageDetails) => (this.pageDetails = pageDetails)); - - this.hostname = Utils.getHostname(this.url); - const otherTypes: CipherType[] = []; - const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$)); - const dontShowIdentities = !(await firstValueFrom( - this.vaultSettingsService.showIdentitiesCurrentTab$, - )); - this.showOrganizations = await this.organizationService.hasOrganizations(); - if (!dontShowCards) { - otherTypes.push(CipherType.Card); - } - if (!dontShowIdentities) { - otherTypes.push(CipherType.Identity); - } - - const ciphers = await this.cipherService.getAllDecryptedForUrl( - this.url, - otherTypes.length > 0 ? otherTypes : null, - ); - - this.loginCiphers = []; - this.cardCiphers = []; - this.identityCiphers = []; - - ciphers.forEach((c) => { - if (!this.vaultFilterService.filterCipherForSelectedVault(c)) { - switch (c.type) { - case CipherType.Login: - this.loginCiphers.push(c); - break; - case CipherType.Card: - this.cardCiphers.push(c); - break; - case CipherType.Identity: - this.identityCiphers.push(c); - break; - default: - break; - } - } - }); - - if (this.loginCiphers.length) { - this.loginCiphers = this.loginCiphers.sort((a, b) => - this.cipherService.sortCiphersByLastUsedThenName(a, b), - ); - } - - this.isLoading = this.loaded = true; - } - - async goToSettings() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["autofill"]); - } - - async dismissCallout() { - await this.autofillSettingsService.setAutofillOnPageLoadCalloutIsDismissed(true); - this.showHowToAutofill = false; - } - - private async setCallout() { - const inlineMenuVisibilityIsOff = - (await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$)) === - AutofillOverlayVisibility.Off; - - this.showHowToAutofill = - this.loginCiphers.length > 0 && - inlineMenuVisibilityIsOff && - !(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)) && - !(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadCalloutIsDismissed$)); - - if (this.showHowToAutofill) { - const autofillCommand = await this.platformUtilsService.getAutofillKeyboardShortcut(); - await this.setAutofillCalloutText(autofillCommand); - } - } - - private setAutofillCalloutText(command: string) { - if (command) { - this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithCommand", command); - } else { - this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand"); - } - } -} diff --git a/apps/browser/src/vault/popup/components/vault/password-history.component.html b/apps/browser/src/vault/popup/components/vault/password-history.component.html deleted file mode 100644 index 6286aa1022d..00000000000 --- a/apps/browser/src/vault/popup/components/vault/password-history.component.html +++ /dev/null @@ -1,40 +0,0 @@ -
    -
    - -
    -

    - {{ "passwordHistory" | i18n }} -

    -
    -
    -
    -
    -
    -
    -
    -
    - - {{ h.lastUsedDate | date: "medium" }} -
    -
    -
    - -
    -
    -
    -
    -
    -

    {{ "noPasswordsInList" | i18n }}

    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/password-history.component.ts b/apps/browser/src/vault/popup/components/vault/password-history.component.ts deleted file mode 100644 index bf1b4ea7717..00000000000 --- a/apps/browser/src/vault/popup/components/vault/password-history.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Location } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; - -@Component({ - selector: "app-password-history", - templateUrl: "password-history.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class PasswordHistoryComponent extends BasePasswordHistoryComponent implements OnInit { - constructor( - cipherService: CipherService, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - accountService: AccountService, - private location: Location, - private route: ActivatedRoute, - ) { - super(cipherService, platformUtilsService, i18nService, accountService, window); - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - if (params.cipherId) { - this.cipherId = params.cipherId; - } else { - this.close(); - } - await this.init(); - }); - } - - close() { - this.location.back(); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/share.component.html b/apps/browser/src/vault/popup/components/vault/share.component.html deleted file mode 100644 index 46aaecd06b8..00000000000 --- a/apps/browser/src/vault/popup/components/vault/share.component.html +++ /dev/null @@ -1,77 +0,0 @@ -
    - -
    -
    - -
    -

    - {{ "moveToOrganization" | i18n }} -

    -
    - -
    -
    -
    -
    -
    -
    - {{ "noOrganizationsList" | i18n }} -
    -
    -
    -
    - - -
    -
    - -
    -
    -

    - {{ "collections" | i18n }} -

    -
    -
    - {{ "noCollectionsInList" | i18n }} -
    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/share.component.ts b/apps/browser/src/vault/popup/components/vault/share.component.ts deleted file mode 100644 index 8e061665b73..00000000000 --- a/apps/browser/src/vault/popup/components/vault/share.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { CollectionService } from "@bitwarden/admin-console/common"; -import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; - -@Component({ - selector: "app-vault-share", - templateUrl: "share.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ShareComponent extends BaseShareComponent implements OnInit { - constructor( - collectionService: CollectionService, - platformUtilsService: PlatformUtilsService, - i18nService: I18nService, - logService: LogService, - cipherService: CipherService, - private route: ActivatedRoute, - private router: Router, - organizationService: OrganizationService, - accountService: AccountService, - ) { - super( - collectionService, - platformUtilsService, - i18nService, - cipherService, - logService, - organizationService, - accountService, - ); - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.onSharedCipher.subscribe(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["view-cipher", { cipherId: this.cipherId }]); - }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - this.cipherId = params.cipherId; - await this.load(); - }); - } - - async submit(): Promise { - const success = await super.submit(); - if (success) { - this.cancel(); - } - return success; - } - - cancel() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { - replaceUrl: true, - queryParams: { cipherId: this.cipher.id }, - }); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html b/apps/browser/src/vault/popup/components/vault/vault-filter.component.html deleted file mode 100644 index bf557f74608..00000000000 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.html +++ /dev/null @@ -1,238 +0,0 @@ - -
    - -
    -

    {{ "myVault" | i18n }}

    - -
    - -
    -
    -
    - -
    - - - -

    {{ "noItemsInList" | i18n }}

    - -
    -
    - -
    -

    - {{ "favorites" | i18n }} - {{ favoriteCiphers.length }} -

    -
    - - -
    -
    -
    -

    - {{ "types" | i18n }} - 4 -

    -
    - - - - - -
    -
    -
    -

    - {{ "folders" | i18n }} - {{ folderCount }} -

    -
    - -
    -
    -
    -

    - {{ "collections" | i18n }} - {{ nestedCollections.length }} -

    -
    - -
    -
    -
    -

    - {{ "noneFolder" | i18n }} -
    {{ noFolderCiphers.length }}
    -

    -
    - - -
    -
    -
    -

    - {{ "trash" | i18n }} - {{ deletedCount }} -

    -
    - -
    -
    -
    - -
    -

    {{ "noItemsInList" | i18n }}

    -
    - -
    -
    - - -
    -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts deleted file mode 100644 index d430568869c..00000000000 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts +++ /dev/null @@ -1,482 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Location } from "@angular/common"; -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; - -import { CollectionView } from "@bitwarden/admin-console/common"; -import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -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 { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; - -import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState"; -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { VaultBrowserStateService } from "../../../services/vault-browser-state.service"; -import { VaultFilterService } from "../../../services/vault-filter.service"; - -const ComponentId = "VaultComponent"; - -@Component({ - selector: "app-vault-filter", - templateUrl: "vault-filter.component.html", -}) -export class VaultFilterComponent implements OnInit, OnDestroy { - get showNoFolderCiphers(): boolean { - return ( - this.noFolderCiphers != null && - this.noFolderCiphers.length < this.noFolderListSize && - this.collections.length === 0 - ); - } - - get folderCount(): number { - return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1); - } - folders: FolderView[]; - nestedFolders: TreeNode[]; - collections: CollectionView[]; - nestedCollections: TreeNode[]; - loaded = false; - cipherType = CipherType; - ciphers: CipherView[]; - favoriteCiphers: CipherView[]; - noFolderCiphers: CipherView[]; - folderCounts = new Map(); - collectionCounts = new Map(); - typeCounts = new Map(); - state: BrowserGroupingsComponentState; - showLeftHeader = true; - searchPending = false; - searchTypeSearch = false; - deletedCount = 0; - vaultFilter: VaultFilter; - selectedOrganization: string = null; - showCollections = true; - - isSshKeysEnabled = false; - - private loadedTimeout: number; - private selectedTimeout: number; - private preventSelected = false; - private noFolderListSize = 100; - private searchTimeout: any = null; - private hasSearched = false; - private hasLoadedAllCiphers = false; - private allCiphers: CipherView[] = null; - private destroy$ = new Subject(); - private _searchText$ = new BehaviorSubject(""); - private isSearchable: boolean = false; - - get searchText() { - return this._searchText$.value; - } - set searchText(value: string) { - this._searchText$.next(value); - } - - constructor( - private i18nService: I18nService, - private cipherService: CipherService, - private router: Router, - private ngZone: NgZone, - private broadcasterService: BroadcasterService, - private changeDetectorRef: ChangeDetectorRef, - private route: ActivatedRoute, - private syncService: SyncService, - private platformUtilsService: PlatformUtilsService, - private searchService: SearchService, - private location: Location, - private vaultFilterService: VaultFilterService, - private vaultBrowserStateService: VaultBrowserStateService, - private configService: ConfigService, - ) { - this.noFolderListSize = 100; - } - - async ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - this.showLeftHeader = !( - BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox() - ); - await this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null); - - this.broadcasterService.subscribe(ComponentId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - window.setTimeout(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - }, 500); - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - - const restoredScopeState = await this.restoreState(); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState(); - if (this.state?.searchText) { - this.searchText = this.state.searchText; - } else if (params.searchText) { - this.searchText = params.searchText; - this.location.replaceState("vault"); - } - - if (!this.syncService.syncInProgress) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } else { - this.loadedTimeout = window.setTimeout(() => { - if (!this.loaded) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } - }, 5000); - } - - if (!this.syncService.syncInProgress || restoredScopeState) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY); - } - }); - - this._searchText$ - .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(searchText))), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearchable = isSearchable; - }); - - this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem); - } - - ngOnDestroy() { - if (this.loadedTimeout != null) { - window.clearTimeout(this.loadedTimeout); - } - if (this.selectedTimeout != null) { - window.clearTimeout(this.selectedTimeout); - } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.saveState(); - this.broadcasterService.unsubscribe(ComponentId); - this.destroy$.next(); - this.destroy$.complete(); - } - - async load() { - this.vaultFilter = this.vaultFilterService.getVaultFilter(); - - this.updateSelectedOrg(); - await this.loadCollectionsAndFolders(); - await this.loadCiphers(); - - if (this.showNoFolderCiphers && this.nestedFolders.length > 0) { - // Remove "No Folder" from folder listing - this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1); - } - - this.loaded = true; - } - - async loadCiphers() { - this.allCiphers = await this.cipherService.getAllDecrypted(); - if (!this.hasLoadedAllCiphers) { - this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText)); - } - await this.search(null); - this.getCounts(); - } - - async loadCollections() { - const allCollections = await this.vaultFilterService.buildCollections( - this.selectedOrganization, - ); - this.collections = allCollections.fullList; - this.nestedCollections = allCollections.nestedList; - } - - async loadFolders() { - const allFolders = await firstValueFrom( - this.vaultFilterService.buildNestedFolders(this.selectedOrganization), - ); - this.folders = allFolders.fullList; - this.nestedFolders = allFolders.nestedList; - } - - async search(timeout: number = null) { - this.searchPending = false; - if (this.searchTimeout != null) { - clearTimeout(this.searchTimeout); - } - const filterDeleted = (c: CipherView) => !c.isDeleted; - if (timeout == null) { - this.hasSearched = this.isSearchable; - this.ciphers = await this.searchService.searchCiphers( - this.searchText, - filterDeleted, - this.allCiphers, - ); - this.ciphers = this.ciphers.filter( - (c) => !this.vaultFilterService.filterCipherForSelectedVault(c), - ); - return; - } - this.searchPending = true; - this.searchTimeout = setTimeout(async () => { - this.hasSearched = this.isSearchable; - if (!this.hasLoadedAllCiphers && !this.hasSearched) { - await this.loadCiphers(); - } else { - this.ciphers = await this.searchService.searchCiphers( - this.searchText, - filterDeleted, - this.allCiphers, - ); - } - this.ciphers = this.ciphers.filter( - (c) => !this.vaultFilterService.filterCipherForSelectedVault(c), - ); - this.searchPending = false; - }, timeout); - } - - async selectType(type: CipherType) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/ciphers"], { queryParams: { type: type } }); - } - - async selectFolder(folder: FolderView) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id || "none" } }); - } - - async selectCollection(collection: CollectionView) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } }); - } - - async selectTrash() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/ciphers"], { queryParams: { deleted: true } }); - } - - async selectCipher(cipher: CipherView) { - this.selectedTimeout = window.setTimeout(() => { - if (!this.preventSelected) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); - } - this.preventSelected = false; - }, 200); - } - - async launchCipher(cipher: CipherView) { - if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) { - return; - } - - if (this.selectedTimeout != null) { - window.clearTimeout(this.selectedTimeout); - } - this.preventSelected = true; - await this.cipherService.updateLastLaunchedDate(cipher.id); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab(cipher.login.launchUri); - if (BrowserPopupUtils.inPopup(window)) { - BrowserApi.closePopup(window); - } - } - - async addCipher() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-cipher"], { - queryParams: { selectedVault: this.vaultFilter.selectedOrganizationId }, - }); - } - - async vaultFilterChanged() { - if (this.showSearching) { - await this.search(); - } - this.updateSelectedOrg(); - await this.loadCollectionsAndFolders(); - this.getCounts(); - } - - updateSelectedOrg() { - this.vaultFilter = this.vaultFilterService.getVaultFilter(); - if (this.vaultFilter.selectedOrganizationId != null) { - this.selectedOrganization = this.vaultFilter.selectedOrganizationId; - } else { - this.selectedOrganization = null; - } - } - - getCounts() { - let favoriteCiphers: CipherView[] = null; - let noFolderCiphers: CipherView[] = null; - const folderCounts = new Map(); - const collectionCounts = new Map(); - const typeCounts = new Map(); - - this.deletedCount = this.allCiphers.filter( - (c) => c.isDeleted && !this.vaultFilterService.filterCipherForSelectedVault(c), - ).length; - - this.ciphers?.forEach((c) => { - if (!this.vaultFilterService.filterCipherForSelectedVault(c)) { - if (c.isDeleted) { - return; - } - if (c.favorite) { - if (favoriteCiphers == null) { - favoriteCiphers = []; - } - favoriteCiphers.push(c); - } - - if (c.folderId == null) { - if (noFolderCiphers == null) { - noFolderCiphers = []; - } - noFolderCiphers.push(c); - } - - if (typeCounts.has(c.type)) { - typeCounts.set(c.type, typeCounts.get(c.type) + 1); - } else { - typeCounts.set(c.type, 1); - } - - if (folderCounts.has(c.folderId)) { - folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1); - } else { - folderCounts.set(c.folderId, 1); - } - - if (c.collectionIds != null) { - c.collectionIds.forEach((colId) => { - if (collectionCounts.has(colId)) { - collectionCounts.set(colId, collectionCounts.get(colId) + 1); - } else { - collectionCounts.set(colId, 1); - } - }); - } - } - }); - - this.favoriteCiphers = favoriteCiphers; - this.noFolderCiphers = noFolderCiphers; - this.typeCounts = typeCounts; - this.folderCounts = folderCounts; - this.collectionCounts = collectionCounts; - } - - showSearching() { - return this.hasSearched || (!this.searchPending && this.isSearchable); - } - - closeOnEsc(e: KeyboardEvent) { - // If input not empty, use browser default behavior of clearing input instead - if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) { - BrowserApi.closePopup(window); - } - } - - private async loadCollectionsAndFolders() { - this.showCollections = !this.vaultFilter.myVaultOnly; - await this.loadFolders(); - await this.loadCollections(); - } - - private async saveState() { - this.state = Object.assign(new BrowserGroupingsComponentState(), { - scrollY: BrowserPopupUtils.getContentScrollY(window), - searchText: this.searchText, - favoriteCiphers: this.favoriteCiphers, - noFolderCiphers: this.noFolderCiphers, - ciphers: this.ciphers, - collectionCounts: this.collectionCounts, - folderCounts: this.folderCounts, - typeCounts: this.typeCounts, - folders: this.folders, - collections: this.collections, - deletedCount: this.deletedCount, - }); - await this.vaultBrowserStateService.setBrowserGroupingsComponentState(this.state); - } - - private async restoreState(): Promise { - this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState(); - if (this.state == null) { - return false; - } - - if (this.state.favoriteCiphers != null) { - this.favoriteCiphers = this.state.favoriteCiphers; - } - if (this.state.noFolderCiphers != null) { - this.noFolderCiphers = this.state.noFolderCiphers; - } - if (this.state.ciphers != null) { - this.ciphers = this.state.ciphers; - } - if (this.state.collectionCounts != null) { - this.collectionCounts = this.state.collectionCounts; - } - if (this.state.folderCounts != null) { - this.folderCounts = this.state.folderCounts; - } - if (this.state.typeCounts != null) { - this.typeCounts = this.state.typeCounts; - } - if (this.state.folders != null) { - this.folders = this.state.folders; - } - if (this.state.collections != null) { - this.collections = this.state.collections; - } - if (this.state.deletedCount != null) { - this.deletedCount = this.state.deletedCount; - } - - return true; - } -} diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.html b/apps/browser/src/vault/popup/components/vault/vault-items.component.html deleted file mode 100644 index f10688554d9..00000000000 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.html +++ /dev/null @@ -1,123 +0,0 @@ -
    -
    - -
    -

    {{ "myVault" | i18n }}

    - -
    - -
    -
    -
    - - -
    -

    - {{ "folders" | i18n }} -

    -
    - -
    -
    -
    -

    - {{ "collections" | i18n }} -

    -
    - -
    -
    -
    - -
    - -
    - - - -

    {{ "noItemsInList" | i18n }}

    - -
    -
    -
    - -
    -

    - {{ groupingTitle }} - {{ isSearching() ? ciphers.length : ciphers.length }} -

    -
    - -
    -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts deleted file mode 100644 index 387afcfe217..00000000000 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ /dev/null @@ -1,316 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Location } from "@angular/common"; -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; -import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { CipherType } from "@bitwarden/common/vault/enums"; -import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; - -import { BrowserComponentState } from "../../../../models/browserComponentState"; -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { VaultBrowserStateService } from "../../../services/vault-browser-state.service"; -import { VaultFilterService } from "../../../services/vault-filter.service"; - -const ComponentId = "VaultItemsComponent"; - -@Component({ - selector: "app-vault-items", - templateUrl: "vault-items.component.html", -}) -export class VaultItemsComponent extends BaseVaultItemsComponent implements OnInit, OnDestroy { - groupingTitle: string; - state: BrowserComponentState; - folderId: string = null; - collectionId: string = null; - type: CipherType = null; - nestedFolders: TreeNode[]; - nestedCollections: TreeNode[]; - searchTypeSearch = false; - showOrganizations = false; - vaultFilter: VaultFilter; - deleted = true; - noneFolder = false; - showVaultFilter = false; - - private selectedTimeout: number; - private preventSelected = false; - private applySavedState = true; - private scrollingContainer = "cdk-virtual-scroll-viewport"; - - constructor( - searchService: SearchService, - private organizationService: OrganizationService, - private route: ActivatedRoute, - private router: Router, - private location: Location, - private ngZone: NgZone, - private broadcasterService: BroadcasterService, - private changeDetectorRef: ChangeDetectorRef, - private stateService: VaultBrowserStateService, - private i18nService: I18nService, - private collectionService: CollectionService, - private platformUtilsService: PlatformUtilsService, - cipherService: CipherService, - private vaultFilterService: VaultFilterService, - ) { - super(searchService, cipherService); - this.applySavedState = - (window as any).previousPopupUrl != null && - !(window as any).previousPopupUrl.startsWith("/ciphers"); - } - - async ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - this.showOrganizations = await this.organizationService.hasOrganizations(); - this.vaultFilter = this.vaultFilterService.getVaultFilter(); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - if (this.applySavedState) { - this.state = await this.stateService.getBrowserVaultItemsComponentState(); - if (this.state?.searchText) { - this.searchText = this.state.searchText; - } - } - - if (params.deleted) { - this.showVaultFilter = true; - this.groupingTitle = this.i18nService.t("trash"); - this.searchPlaceholder = this.i18nService.t("searchTrash"); - await this.load(this.buildFilter(), true); - } else if (params.type) { - this.showVaultFilter = true; - this.searchPlaceholder = this.i18nService.t("searchType"); - this.type = parseInt(params.type, null); - switch (this.type) { - case CipherType.Login: - this.groupingTitle = this.i18nService.t("logins"); - break; - case CipherType.Card: - this.groupingTitle = this.i18nService.t("cards"); - break; - case CipherType.Identity: - this.groupingTitle = this.i18nService.t("identities"); - break; - case CipherType.SecureNote: - this.groupingTitle = this.i18nService.t("secureNotes"); - break; - case CipherType.SshKey: - this.groupingTitle = this.i18nService.t("sshKeys"); - break; - default: - break; - } - await this.load(this.buildFilter()); - } else if (params.folderId) { - this.showVaultFilter = true; - this.folderId = params.folderId === "none" ? null : params.folderId; - this.searchPlaceholder = this.i18nService.t("searchFolder"); - if (this.folderId != null) { - this.showOrganizations = false; - const folderNode = await this.vaultFilterService.getFolderNested(this.folderId); - if (folderNode != null && folderNode.node != null) { - this.groupingTitle = folderNode.node.name; - this.nestedFolders = - folderNode.children != null && folderNode.children.length > 0 - ? folderNode.children - : null; - } - } else { - this.noneFolder = true; - this.groupingTitle = this.i18nService.t("noneFolder"); - } - await this.load(this.buildFilter()); - } else if (params.collectionId) { - this.showVaultFilter = false; - this.collectionId = params.collectionId; - this.searchPlaceholder = this.i18nService.t("searchCollection"); - const collectionNode = await this.collectionService.getNested(this.collectionId); - if (collectionNode != null && collectionNode.node != null) { - this.groupingTitle = collectionNode.node.name; - this.nestedCollections = - collectionNode.children != null && collectionNode.children.length > 0 - ? collectionNode.children - : null; - } - await this.load( - (c) => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1, - ); - } else { - this.showVaultFilter = true; - this.groupingTitle = this.i18nService.t("allItems"); - await this.load(this.buildFilter()); - } - - if (this.applySavedState && this.state != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserPopupUtils.setContentScrollY(window, this.state.scrollY, { - delay: 0, - containerSelector: this.scrollingContainer, - }); - } - await this.stateService.setBrowserVaultItemsComponentState(null); - }); - - this.broadcasterService.subscribe(ComponentId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - if (message.successfully) { - window.setTimeout(() => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.refresh(); - }, 500); - } - break; - default: - break; - } - - this.changeDetectorRef.detectChanges(); - }); - }); - } - - ngOnDestroy() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.saveState(); - this.broadcasterService.unsubscribe(ComponentId); - } - - selectCipher(cipher: CipherView) { - this.selectedTimeout = window.setTimeout(() => { - if (!this.preventSelected) { - super.selectCipher(cipher); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { - queryParams: { cipherId: cipher.id, collectionId: this.collectionId }, - }); - } - this.preventSelected = false; - }, 200); - } - - selectFolder(folder: FolderView) { - if (folder.id != null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id } }); - } - } - - selectCollection(collection: CollectionView) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } }); - } - - async launchCipher(cipher: CipherView) { - if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) { - return; - } - - if (this.selectedTimeout != null) { - window.clearTimeout(this.selectedTimeout); - } - this.preventSelected = true; - await this.cipherService.updateLastLaunchedDate(cipher.id); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab(cipher.login.launchUri); - if (BrowserPopupUtils.inPopup(window)) { - BrowserApi.closePopup(window); - } - } - - addCipher() { - if (this.deleted) { - return false; - } - super.addCipher(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-cipher"], { - queryParams: { - folderId: this.folderId, - type: this.type, - collectionId: this.collectionId, - selectedVault: this.vaultFilter.selectedOrganizationId, - }, - }); - } - - back() { - (window as any).routeDirection = "b"; - this.location.back(); - } - - showGroupings() { - return ( - !this.isSearching() && - ((this.nestedFolders && this.nestedFolders.length) || - (this.nestedCollections && this.nestedCollections.length)) - ); - } - - async changeVaultSelection() { - this.vaultFilter = this.vaultFilterService.getVaultFilter(); - await this.load(this.buildFilter(), this.deleted); - } - - private buildFilter(): (cipher: CipherView) => boolean { - return (cipher) => { - let cipherPassesFilter = true; - if (this.deleted && cipherPassesFilter) { - cipherPassesFilter = cipher.isDeleted; - } - if (this.type != null && cipherPassesFilter) { - cipherPassesFilter = cipher.type === this.type; - } - if (this.folderId != null && this.folderId != "none" && cipherPassesFilter) { - cipherPassesFilter = cipher.folderId === this.folderId; - } - if (this.noneFolder) { - cipherPassesFilter = cipher.folderId == null; - } - if (this.collectionId != null && cipherPassesFilter) { - cipherPassesFilter = - cipher.collectionIds != null && cipher.collectionIds.indexOf(this.collectionId) > -1; - } - if (this.vaultFilter.selectedOrganizationId != null && cipherPassesFilter) { - cipherPassesFilter = cipher.organizationId === this.vaultFilter.selectedOrganizationId; - } - if (this.vaultFilter.myVaultOnly && cipherPassesFilter) { - cipherPassesFilter = cipher.organizationId === null; - } - return cipherPassesFilter; - }; - } - - private async saveState() { - this.state = { - scrollY: BrowserPopupUtils.getContentScrollY(window, this.scrollingContainer), - searchText: this.searchText, - }; - await this.stateService.setBrowserVaultItemsComponentState(this.state); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/vault-select.component.html b/apps/browser/src/vault/popup/components/vault/vault-select.component.html deleted file mode 100644 index 4f6ce3a11e6..00000000000 --- a/apps/browser/src/vault/popup/components/vault/vault-select.component.html +++ /dev/null @@ -1,82 +0,0 @@ - -
    - - - - - - -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/vault-select.component.ts b/apps/browser/src/vault/popup/components/vault/vault-select.component.ts deleted file mode 100644 index 3c5061a516f..00000000000 --- a/apps/browser/src/vault/popup/components/vault/vault-select.component.ts +++ /dev/null @@ -1,227 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition, Overlay, OverlayRef } from "@angular/cdk/overlay"; -import { TemplatePortal } from "@angular/cdk/portal"; -import { - Component, - ElementRef, - EventEmitter, - HostListener, - OnDestroy, - OnInit, - Output, - TemplateRef, - ViewChild, - ViewContainerRef, -} from "@angular/core"; -import { - BehaviorSubject, - combineLatest, - concatMap, - map, - merge, - Observable, - Subject, - takeUntil, -} from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { VaultFilterService } from "../../../services/vault-filter.service"; - -@Component({ - selector: "app-vault-select", - templateUrl: "vault-select.component.html", - animations: [ - trigger("transformPanel", [ - state( - "void", - style({ - opacity: 0, - }), - ), - transition( - "void => open", - animate( - "100ms linear", - style({ - opacity: 1, - }), - ), - ), - transition("* => void", animate("100ms linear", style({ opacity: 0 }))), - ]), - ], -}) -export class VaultSelectComponent implements OnInit, OnDestroy { - @Output() onVaultSelectionChanged = new EventEmitter(); - - @ViewChild("toggleVaults", { read: ElementRef }) - buttonRef: ElementRef; - @ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef; - - private _selectedVault = new BehaviorSubject(null); - - isOpen = false; - loaded = false; - organizations$: Observable; - selectedVault$: Observable = this._selectedVault.asObservable(); - - enforcePersonalOwnership = false; - overlayPosition: ConnectedPosition[] = [ - { - originX: "start", - originY: "bottom", - overlayX: "start", - overlayY: "top", - }, - ]; - - private overlayRef: OverlayRef; - private _destroy = new Subject(); - - shouldShow(organizations: Organization[]): boolean { - return ( - (organizations.length > 0 && !this.enforcePersonalOwnership) || - (organizations.length > 1 && this.enforcePersonalOwnership) - ); - } - - constructor( - private vaultFilterService: VaultFilterService, - private i18nService: I18nService, - private overlay: Overlay, - private viewContainerRef: ViewContainerRef, - private platformUtilsService: PlatformUtilsService, - private organizationService: OrganizationService, - private policyService: PolicyService, - ) {} - - @HostListener("document:keydown.escape", ["$event"]) - handleKeyboardEvent(event: KeyboardEvent) { - if (this.isOpen) { - event.preventDefault(); - this.close(); - } - } - - async ngOnInit() { - this.organizations$ = this.organizationService.memberOrganizations$ - .pipe(takeUntil(this._destroy)) - .pipe(map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name")))); - - combineLatest([ - this.organizations$, - this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), - ]) - .pipe( - concatMap(async ([organizations, enforcePersonalOwnership]) => { - this.enforcePersonalOwnership = enforcePersonalOwnership; - - if (this.shouldShow(organizations)) { - if (this.enforcePersonalOwnership && !this.vaultFilterService.vaultFilter.myVaultOnly) { - const firstOrganization = organizations[0]; - this._selectedVault.next(firstOrganization.name); - this.vaultFilterService.setVaultFilter(firstOrganization.id); - } else if (this.vaultFilterService.vaultFilter.myVaultOnly) { - this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault)); - } else if (this.vaultFilterService.vaultFilter.selectedOrganizationId != null) { - const selectedOrganization = organizations.find( - (o) => o.id === this.vaultFilterService.vaultFilter.selectedOrganizationId, - ); - this._selectedVault.next(selectedOrganization.name); - } else { - this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults)); - } - } - }), - ) - .pipe(takeUntil(this._destroy)) - .subscribe(); - - this.loaded = true; - } - - ngOnDestroy(): void { - this._destroy.next(); - this._destroy.complete(); - this._selectedVault.complete(); - } - - openOverlay() { - const viewPortHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); - const positionStrategyBuilder = this.overlay.position(); - - const positionStrategy = positionStrategyBuilder - .flexibleConnectedTo(this.buttonRef.nativeElement) - .withFlexibleDimensions(true) - .withPush(true) - .withViewportMargin(10) - .withGrowAfterOpen(true) - .withPositions(this.overlayPosition); - - this.overlayRef = this.overlay.create({ - hasBackdrop: true, - positionStrategy, - maxHeight: viewPortHeight - 160, - backdropClass: "cdk-overlay-transparent-backdrop", - scrollStrategy: this.overlay.scrollStrategies.close(), - }); - - const templatePortal = new TemplatePortal(this.templateRef, this.viewContainerRef); - this.overlayRef.attach(templatePortal); - this.isOpen = true; - - // Handle closing - merge( - this.overlayRef.outsidePointerEvents(), - this.overlayRef.backdropClick(), - this.overlayRef.detachments(), - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - ).subscribe(() => { - this.close(); - }); - } - - close() { - if (this.overlayRef) { - this.overlayRef.dispose(); - this.overlayRef = undefined; - } - this.isOpen = false; - } - - selectOrganization(organization: Organization) { - if (!organization.enabled) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("disabledOrganizationFilterError"), - ); - } else { - this._selectedVault.next(organization.name); - this.vaultFilterService.setVaultFilter(organization.id); - this.onVaultSelectionChanged.emit(); - this.close(); - } - } - selectAllVaults() { - this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults)); - this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults); - this.onVaultSelectionChanged.emit(); - this.close(); - } - selectMyVault() { - this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault)); - this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault); - this.onVaultSelectionChanged.emit(); - this.close(); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/view-custom-fields.component.html b/apps/browser/src/vault/popup/components/vault/view-custom-fields.component.html deleted file mode 100644 index 4fbca28734b..00000000000 --- a/apps/browser/src/vault/popup/components/vault/view-custom-fields.component.html +++ /dev/null @@ -1,98 +0,0 @@ - -

    - {{ "customFields" | i18n }} -

    -
    -
    -
    - {{ field.name }} - {{ field.name }} -
    - {{ field.value || " " }} -
    -
    - {{ field.maskedValue }} - - -
    -
    - - - {{ field.value }} -
    -
    -
    - - {{ "linkedValue" | i18n }} -
    - {{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }} -
    -
    -
    - - - -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/view-custom-fields.component.ts b/apps/browser/src/vault/popup/components/vault/view-custom-fields.component.ts deleted file mode 100644 index 249f83c4444..00000000000 --- a/apps/browser/src/vault/popup/components/vault/view-custom-fields.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Component } from "@angular/core"; - -import { ViewCustomFieldsComponent as BaseViewCustomFieldsComponent } from "@bitwarden/angular/vault/components/view-custom-fields.component"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; - -@Component({ - selector: "app-vault-view-custom-fields", - templateUrl: "view-custom-fields.component.html", -}) -export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent { - constructor(eventCollectionService: EventCollectionService) { - super(eventCollectionService); - } -} diff --git a/apps/browser/src/vault/popup/components/vault/view.component.html b/apps/browser/src/vault/popup/components/vault/view.component.html deleted file mode 100644 index 57a5d007d8a..00000000000 --- a/apps/browser/src/vault/popup/components/vault/view.component.html +++ /dev/null @@ -1,719 +0,0 @@ -
    -
    - -
    -

    - {{ "viewItem" | i18n }} -

    -
    - -
    -
    -
    -
    -

    - {{ "itemInformation" | i18n }} -

    -
    -
    - - -
    - -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - {{ "password" | i18n }} -
    - {{ cipher.login.maskedPassword }} -
    -
    -
    -
    -
    - - - - -
    -
    - - -
    -
    -
    - {{ "typePasskey" | i18n }} - {{ fido2CredentialCreationDateValue }} -
    -
    -
    - -
    -
    - {{ "verificationCodeTotp" | i18n }} - {{ totpCodeFormatted }} -
    - -
    - -
    -
    -
    -
    - {{ "verificationCodeTotp" | i18n }} - - - {{ "premiumSubcriptionRequired" | i18n }} - - -
    -
    -
    - -
    -
    - {{ "cardholderName" | i18n }} - {{ cipher.card.cardholderName }} -
    -
    -
    - {{ "number" | i18n }} - {{ - cipher.card.maskedNumber | creditCardNumber: cipher.card.brand - }} - {{ - cipher.card.number | creditCardNumber: cipher.card.brand - }} -
    -
    - - -
    -
    -
    - {{ "brand" | i18n }} - {{ cipher.card.brand }} -
    -
    - {{ "expiration" | i18n }} - {{ cipher.card.expiration }} -
    -
    -
    - {{ "securityCode" | i18n }} - {{ cipher.card.maskedCode }} - {{ cipher.card.code }} -
    -
    - - -
    -
    -
    - -
    -
    - {{ "identityName" | i18n }} - {{ cipher.identity.fullName }} -
    -
    - {{ "username" | i18n }} - {{ cipher.identity.username }} -
    -
    - {{ "company" | i18n }} - {{ cipher.identity.company }} -
    -
    - {{ "ssn" | i18n }} - {{ cipher.identity.ssn }} -
    -
    - {{ "passportNumber" | i18n }} - {{ cipher.identity.passportNumber }} -
    -
    - {{ "licenseNumber" | i18n }} - {{ cipher.identity.licenseNumber }} -
    -
    - {{ "email" | i18n }} - {{ cipher.identity.email }} -
    -
    - {{ "phone" | i18n }} - {{ cipher.identity.phone }} -
    -
    - {{ "address" | i18n }} -
    {{ cipher.identity.address1 }}
    -
    {{ cipher.identity.address2 }}
    -
    {{ cipher.identity.address3 }}
    -
    {{ cipher.identity.fullAddressPart2 }}
    -
    {{ cipher.identity.country }}
    -
    -
    - -
    -
    - - {{ "sshPrivateKey" | i18n }} - -
    -
    -
    - - {{ "sshPublicKey" | i18n }} - {{ cipher.sshKey.publicKey }} -
    -
    - - {{ "sshFingerprint" | i18n }} - {{ cipher.sshKey.keyFingerprint }} -
    -
    -
    -
    -
    -
    -
    -
    - - - - - -
    -
    - - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -

    - -

    -
    -
    - -
    -
    -
    -
    - -
    -
    -

    - {{ "attachments" | i18n }} -

    -
    - -
    -
    -
    -
    - - - - - - -
    -
    -
    - -
    -
    diff --git a/apps/browser/src/vault/popup/components/vault/view.component.ts b/apps/browser/src/vault/popup/components/vault/view.component.ts deleted file mode 100644 index 242cff03c75..00000000000 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ /dev/null @@ -1,443 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { DatePipe, Location } from "@angular/common"; -import { ChangeDetectorRef, Component, NgZone, OnInit, OnDestroy } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs"; -import { first, map } from "rxjs/operators"; - -import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; -import { PasswordRepromptService } from "@bitwarden/vault"; - -import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service"; -import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service"; -import { BrowserApi } from "../../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data"; -import { closeViewVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window"; - -const BroadcasterSubscriptionId = "ChildViewComponent"; - -export const AUTOFILL_ID = "autofill"; -export const SHOW_AUTOFILL_BUTTON = "show-autofill-button"; -export const COPY_USERNAME_ID = "copy-username"; -export const COPY_PASSWORD_ID = "copy-password"; -export const COPY_VERIFICATION_CODE_ID = "copy-totp"; - -type CopyAction = - | typeof COPY_USERNAME_ID - | typeof COPY_PASSWORD_ID - | typeof COPY_VERIFICATION_CODE_ID; -type LoadAction = typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON | CopyAction; - -@Component({ - selector: "app-vault-view", - templateUrl: "view.component.html", -}) -export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy { - showAttachments = true; - pageDetails: any[] = []; - tab: any; - senderTabId?: number; - loadAction?: LoadAction; - private static readonly copyActions = new Set([ - COPY_USERNAME_ID, - COPY_PASSWORD_ID, - COPY_VERIFICATION_CODE_ID, - ]); - uilocation?: "popout" | "popup" | "sidebar" | "tab"; - loadPageDetailsTimeout: number; - inPopout = false; - cipherType = CipherType; - private fido2PopoutSessionData$ = fido2PopoutSessionData$(); - private collectPageDetailsSubscription: Subscription; - - private destroy$ = new Subject(); - - constructor( - cipherService: CipherService, - folderService: FolderService, - totpService: TotpServiceAbstraction, - tokenService: TokenService, - i18nService: I18nService, - keyService: KeyService, - encryptService: EncryptService, - platformUtilsService: PlatformUtilsService, - auditService: AuditService, - private route: ActivatedRoute, - private router: Router, - private location: Location, - broadcasterService: BroadcasterService, - ngZone: NgZone, - changeDetectorRef: ChangeDetectorRef, - stateService: StateService, - eventCollectionService: EventCollectionService, - private autofillService: AutofillService, - private messagingService: MessagingService, - apiService: ApiService, - passwordRepromptService: PasswordRepromptService, - logService: LogService, - fileDownloadService: FileDownloadService, - dialogService: DialogService, - datePipe: DatePipe, - accountService: AccountService, - billingAccountProfileStateService: BillingAccountProfileStateService, - cipherAuthorizationService: CipherAuthorizationService, - ) { - super( - cipherService, - folderService, - totpService, - tokenService, - i18nService, - keyService, - encryptService, - platformUtilsService, - auditService, - window, - broadcasterService, - ngZone, - changeDetectorRef, - eventCollectionService, - apiService, - passwordRepromptService, - logService, - stateService, - fileDownloadService, - dialogService, - datePipe, - accountService, - billingAccountProfileStateService, - cipherAuthorizationService, - ); - } - - ngOnInit() { - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => { - this.loadAction = value?.action; - this.senderTabId = parseInt(value?.senderTabId, 10) || undefined; - this.uilocation = value?.uilocation; - }); - - this.inPopout = this.uilocation === "popout" || BrowserPopupUtils.inPopout(window); - - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - if (params.cipherId) { - this.cipherId = params.cipherId; - } - - if (params.collectionId) { - this.collectionId = params.collectionId; - } - - if (!params.cipherId) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.close(); - } - - await this.load(); - }); - - super.ngOnInit(); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "tabChanged": - case "windowChanged": - if (this.loadPageDetailsTimeout != null) { - window.clearTimeout(this.loadPageDetailsTimeout); - } - this.loadPageDetailsTimeout = window.setTimeout(() => this.loadPageDetails(), 500); - break; - default: - break; - } - }); - }); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - super.ngOnDestroy(); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - async load() { - await super.load(); - await this.loadPageDetails(); - await this.handleLoadAction(); - } - - async edit() { - if (this.cipher.isDeleted) { - return false; - } - if (!(await super.edit())) { - return false; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/edit-cipher"], { - queryParams: { - cipherId: this.cipher.id, - type: this.cipher.type, - isNew: false, - collectionId: this.collectionId, - }, - }); - return true; - } - - async clone() { - if (this.cipher.isDeleted) { - return false; - } - - if (!(await super.clone())) { - return false; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/clone-cipher"], { - queryParams: { - cloneMode: true, - cipherId: this.cipher.id, - }, - }); - return true; - } - - async share() { - if (!(await super.share())) { - return false; - } - - if (this.cipher.organizationId == null) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/share-cipher"], { - replaceUrl: true, - queryParams: { cipherId: this.cipher.id }, - }); - } - return true; - } - - async fillCipher() { - const didAutofill = await this.doAutofill(); - if (didAutofill) { - this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess")); - } - - return didAutofill; - } - - async fillCipherAndSave() { - const didAutofill = await this.doAutofill(); - - if (didAutofill) { - if (this.tab == null) { - throw new Error("No tab found."); - } - - if (this.cipher.login.uris == null) { - this.cipher.login.uris = []; - } else { - if (this.cipher.login.uris.some((uri) => uri.uri === this.tab.url)) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("autoFillSuccessAndSavedUri"), - ); - return; - } - } - - const loginUri = new LoginUriView(); - loginUri.uri = this.tab.url; - this.cipher.login.uris.push(loginUri); - - try { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const cipher: Cipher = await this.cipherService.encrypt(this.cipher, activeUserId); - await this.cipherService.updateWithServer(cipher); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("autoFillSuccessAndSavedUri"), - ); - this.messagingService.send("editedCipher"); - } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError")); - } - } - } - - async restore() { - if (!this.cipher.isDeleted) { - return false; - } - if (await super.restore()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.close(); - return true; - } - return false; - } - - async delete() { - if (await super.delete()) { - this.messagingService.send("deletedCipher"); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.close(); - return true; - } - return false; - } - - async close() { - const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); - if (this.inPopout && sessionData.isFido2Session) { - BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId); - return; - } - - if ( - BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) && - this.senderTabId - ) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.focusTab(this.senderTabId); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`); - return; - } - - this.location.back(); - } - - private async loadPageDetails() { - this.collectPageDetailsSubscription?.unsubscribe(); - this.pageDetails = []; - this.tab = this.senderTabId - ? await BrowserApi.getTab(this.senderTabId) - : await BrowserApi.getTabFromCurrentWindow(); - - if (!this.tab) { - return; - } - - this.collectPageDetailsSubscription = this.autofillService - .collectPageDetailsFromTab$(this.tab) - .pipe(takeUntil(this.destroy$)) - .subscribe((pageDetails) => (this.pageDetails = pageDetails)); - } - - private async doAutofill() { - const originalTabURL = this.tab.url?.length && new URL(this.tab.url); - - if (!(await this.promptPassword())) { - return false; - } - - const currentTabURL = this.tab.url?.length && new URL(this.tab.url); - - const originalTabHostPath = - originalTabURL && `${originalTabURL.origin}${originalTabURL.pathname}`; - const currentTabHostPath = currentTabURL && `${currentTabURL.origin}${currentTabURL.pathname}`; - - const tabUrlChanged = originalTabHostPath !== currentTabHostPath; - - if (this.pageDetails == null || this.pageDetails.length === 0 || tabUrlChanged) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); - return false; - } - - try { - this.totpCode = await this.autofillService.doAutoFill({ - tab: this.tab, - cipher: this.cipher, - pageDetails: this.pageDetails, - doc: window.document, - fillNewPassword: true, - allowTotpAutofill: true, - }); - if (this.totpCode != null) { - this.platformUtilsService.copyToClipboard(this.totpCode, { window: window }); - } - } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError")); - this.changeDetectorRef.detectChanges(); - return false; - } - - return true; - } - - private async handleLoadAction() { - if (!this.loadAction || this.loadAction === SHOW_AUTOFILL_BUTTON) { - return; - } - - let loadActionSuccess = false; - if (this.loadAction === AUTOFILL_ID) { - loadActionSuccess = await this.fillCipher(); - } - - if (ViewComponent.copyActions.has(this.loadAction)) { - const { username, password } = this.cipher.login; - const copyParams: Record> = { - [COPY_USERNAME_ID]: { value: username, type: "username", name: "Username" }, - [COPY_PASSWORD_ID]: { value: password, type: "password", name: "Password" }, - [COPY_VERIFICATION_CODE_ID]: { - value: this.totpCode, - type: "verificationCodeTotp", - name: "TOTP", - }, - }; - const { value, type, name } = copyParams[this.loadAction as CopyAction]; - loadActionSuccess = await this.copy(value, type, name); - } - - if (this.inPopout) { - setTimeout(() => this.close(), loadActionSuccess ? 1000 : 0); - } - } -} diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts index 2c9afacffd7..2b309e8f817 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts @@ -2,6 +2,7 @@ import { TestBed } from "@angular/core/testing"; import qrcodeParser from "qrcode-parser"; import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { BrowserTotpCaptureService } from "./browser-totp-capture.service"; @@ -13,12 +14,14 @@ describe("BrowserTotpCaptureService", () => { let testBed: TestBed; let service: BrowserTotpCaptureService; let mockCaptureVisibleTab: jest.SpyInstance; + let mockBrowserPopupUtilsInPopout: jest.SpyInstance; const validTotpUrl = "otpauth://totp/label?secret=123"; beforeEach(() => { mockCaptureVisibleTab = jest.spyOn(BrowserApi, "captureVisibleTab"); mockCaptureVisibleTab.mockResolvedValue("screenshot"); + mockBrowserPopupUtilsInPopout = jest.spyOn(BrowserPopupUtils, "inPopout"); testBed = TestBed.configureTestingModule({ providers: [BrowserTotpCaptureService], @@ -66,4 +69,16 @@ describe("BrowserTotpCaptureService", () => { expect(result).toBeNull(); }); + + describe("canCaptureTotp", () => { + it("should return true when not in a popout window", () => { + mockBrowserPopupUtilsInPopout.mockReturnValue(false); + expect(service.canCaptureTotp({} as Window)).toBe(true); + }); + + it("should return false when in a popout window", () => { + mockBrowserPopupUtilsInPopout.mockReturnValue(true); + expect(service.canCaptureTotp({} as Window)).toBe(false); + }); + }); }); diff --git a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts index 3f8ba61ed36..ac73b271c84 100644 --- a/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts @@ -4,6 +4,7 @@ import qrcodeParser from "qrcode-parser"; import { TotpCaptureService } from "@bitwarden/vault"; import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; /** * Implementation of TotpCaptureService for the browser which captures the @@ -20,4 +21,8 @@ export class BrowserTotpCaptureService implements TotpCaptureService { } return null; } + + canCaptureTotp(window: Window) { + return !BrowserPopupUtils.inPopout(window); + } } diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts index 25a7d7594ae..2dad1e3034c 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts @@ -4,6 +4,7 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -20,6 +21,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutoFillOptions, @@ -42,6 +45,7 @@ describe("VaultPopupAutofillService", () => { // Create mocks for VaultPopupAutofillService const mockAutofillService = mock(); + const mockDomainSettingsService = mock(); const mockI18nService = mock(); const mockToastService = mock(); const mockPlatformUtilsService = mock(); @@ -69,6 +73,7 @@ describe("VaultPopupAutofillService", () => { testBed = TestBed.configureTestingModule({ providers: [ { provide: AutofillService, useValue: mockAutofillService }, + { provide: DomainSettingsService, useValue: mockDomainSettingsService }, { provide: I18nService, useValue: mockI18nService }, { provide: ToastService, useValue: mockToastService }, { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts index e4863d5ee43..ff282d7a6d0 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -15,6 +15,7 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -26,6 +27,8 @@ import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view import { ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { AutofillService, @@ -65,6 +68,74 @@ export class VaultPopupAutofillService { shareReplay({ refCount: false, bufferSize: 1 }), ); + currentTabIsOnBlocklist$: Observable = combineLatest([ + this.domainSettingsService.blockedInteractionsUris$, + this.currentAutofillTab$, + ]).pipe( + map(([blockedInteractionsUris, currentTab]) => { + if (blockedInteractionsUris && currentTab?.url?.length) { + const tabURL = new URL(currentTab.url); + const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname); + + if (tabIsBlocked) { + return true; + } + } + + return false; + }), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + showCurrentTabIsBlockedBanner$: Observable = combineLatest([ + this.domainSettingsService.blockedInteractionsUris$, + this.currentAutofillTab$, + ]).pipe( + map(([blockedInteractionsUris, currentTab]) => { + if (blockedInteractionsUris && currentTab?.url?.length) { + const tabURL = new URL(currentTab.url); + const tabIsBlocked = Object.keys(blockedInteractionsUris).includes(tabURL.hostname); + + const showScriptInjectionIsBlockedBanner = + tabIsBlocked && !blockedInteractionsUris[tabURL.hostname]?.bannerIsDismissed; + + return showScriptInjectionIsBlockedBanner; + } + + return false; + }), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + async dismissCurrentTabIsBlockedBanner() { + try { + const currentTab = await firstValueFrom(this.currentAutofillTab$); + const currentTabURL = currentTab?.url.length && new URL(currentTab.url); + + const currentTabHostname = currentTabURL && currentTabURL.hostname; + + if (!currentTabHostname) { + return; + } + + const blockedURIs = await firstValueFrom(this.domainSettingsService.blockedInteractionsUris$); + const tabIsBlocked = Object.keys(blockedURIs).includes(currentTabHostname); + + if (tabIsBlocked) { + void this.domainSettingsService.setBlockedInteractionsUris({ + ...blockedURIs, + [currentTabHostname as string]: { bannerIsDismissed: true }, + }); + } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + throw new Error( + "There was a problem dismissing the blocked interaction URI notification banner", + ); + } + } + /** * Observable that indicates whether autofill is allowed in the current context. * Autofill is allowed when there is a current tab and the popup is not in a popout window. @@ -123,6 +194,7 @@ export class VaultPopupAutofillService { constructor( private autofillService: AutofillService, + private domainSettingsService: DomainSettingsService, private i18nService: I18nService, private toastService: ToastService, private platformUtilService: PlatformUtilsService, diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 5b0eb63998d..528aae111cc 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -15,6 +15,8 @@ import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InlineMenuFieldQualificationService } from "../../../../../browser/src/autofill/services/inline-menu-field-qualification.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -58,6 +60,7 @@ describe("VaultPopupItemsService", () => { cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList); cipherServiceMock.ciphers$ = new BehaviorSubject(null); cipherServiceMock.localData$ = new BehaviorSubject(null); + cipherServiceMock.failedToDecryptCiphers$ = new BehaviorSubject([]); searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers); cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => ciphers.filter((c) => ["0", "1"].includes(c.id)), @@ -294,21 +297,6 @@ describe("VaultPopupItemsService", () => { }); }); - it("should sort by last used then by name by default", (done) => { - service.remainingCiphers$.subscribe(() => { - expect(cipherServiceMock.getLocaleSortingFunction).toHaveBeenCalled(); - done(); - }); - }); - - it("should NOT sort by last used then by name when search text is applied", (done) => { - service.applyFilter("Login"); - service.remainingCiphers$.subscribe(() => { - expect(cipherServiceMock.getLocaleSortingFunction).not.toHaveBeenCalled(); - done(); - }); - }); - it("should filter remainingCiphers$ down to search term", (done) => { const cipherList = Object.values(allCiphers); const searchText = "Login"; @@ -375,20 +363,17 @@ describe("VaultPopupItemsService", () => { }); describe("deletedCiphers$", () => { - it("should return deleted ciphers", (done) => { - const ciphers = [ - { id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true }, - { id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true }, - { id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true }, - { id: "4", type: CipherType.Login, name: "Login 4", isDeleted: false }, - ] as CipherView[]; + it("should return deleted ciphers", async () => { + const deletedCipher = new CipherView(); + deletedCipher.deletedDate = new Date(); + const ciphers = [new CipherView(), new CipherView(), new CipherView(), deletedCipher]; cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers); - service.deletedCiphers$.subscribe((deletedCiphers) => { - expect(deletedCiphers.length).toBe(3); - done(); - }); + (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + + const deletedCiphers = await firstValueFrom(service.deletedCiphers$); + expect(deletedCiphers.length).toBe(1); }); }); 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 93aa8cdaba9..fb230df7953 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 @@ -90,6 +90,8 @@ export class VaultPopupItemsService { tap(() => this._ciphersLoading$.next()), waitUntilSync(this.syncService), switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), + withLatestFrom(this.cipherService.failedToDecryptCiphers$), + map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]), shareReplay({ refCount: true, bufferSize: 1 }), ); @@ -190,11 +192,6 @@ export class VaultPopupItemsService { (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher), ), ), - withLatestFrom(this._hasSearchText$), - map(([ciphers, hasSearchText]) => - // Do not sort alphabetically when there is search text, default to the search service scoring - hasSearchText ? ciphers : ciphers.sort(this.cipherService.getLocaleSortingFunction()), - ), shareReplay({ refCount: false, bufferSize: 1 }), ); @@ -250,8 +247,28 @@ export class VaultPopupItemsService { /** * Observable that contains the list of ciphers that have been deleted. */ - deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( - map((ciphers) => ciphers.filter((c) => c.isDeleted)), + deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( + switchMap((ciphers) => + combineLatest([ + this.organizationService.organizations$, + this.collectionService.decryptedCollections$, + ]).pipe( + map(([organizations, collections]) => { + const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); + const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); + return ciphers + .filter((c) => c.isDeleted) + .map( + (cipher) => + new PopupCipherView( + cipher, + cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]), + orgMap[cipher.organizationId as OrganizationId], + ), + ); + }), + ), + ), shareReplay({ refCount: false, bufferSize: 1 }), ); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 580514de610..e1236be08f9 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -7,9 +7,12 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -32,7 +35,7 @@ describe("VaultPopupListFiltersService", () => { } as unknown as CollectionService; const folderService = { - folderViews$, + folderViews$: () => folderViews$, } as unknown as FolderService; const cipherService = { @@ -60,6 +63,8 @@ describe("VaultPopupListFiltersService", () => { policyAppliesToActiveUser$.next(false); policyService.policyAppliesToActiveUser$.mockClear(); + const accountService = mockAccountServiceWith("userId" as UserId); + collectionService.getAllNested = () => Promise.resolve([]); TestBed.configureTestingModule({ providers: [ @@ -92,6 +97,10 @@ describe("VaultPopupListFiltersService", () => { useValue: { getGlobal: () => ({ state$, update }) }, }, { provide: FormBuilder, useClass: FormBuilder }, + { + provide: AccountService, + useValue: accountService, + }, ], }); @@ -479,7 +488,7 @@ describe("VaultPopupListFiltersService", () => { state$.next(true); service.filterVisibilityState$.subscribe((filterVisibility) => { - expect(filterVisibility).toBeTrue(); + expect(filterVisibility).toBe(true); done(); }); }); @@ -487,7 +496,7 @@ describe("VaultPopupListFiltersService", () => { it("updates stored filter state", async () => { await service.updateFilterVisibility(false); - expect(update).toHaveBeenCalledOnce(); + expect(update).toHaveBeenCalledTimes(1); // Get callback passed to `update` const updateCallback = update.mock.calls[0][0]; expect(updateCallback()).toBe(false); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 0ab08d01e46..8455fd587d0 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -20,6 +20,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -102,6 +103,8 @@ export class VaultPopupListFiltersService { map((ciphers) => Object.values(ciphers)), ); + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + constructor( private folderService: FolderService, private cipherService: CipherService, @@ -111,6 +114,7 @@ export class VaultPopupListFiltersService { private formBuilder: FormBuilder, private policyService: PolicyService, private stateProvider: StateProvider, + private accountService: AccountService, ) { this.filterForm.controls.organization.valueChanges .pipe(takeUntilDestroyed()) @@ -264,61 +268,68 @@ export class VaultPopupListFiltersService { /** * Folder array structured to be directly passed to `ChipSelectComponent` */ - folders$: Observable[]> = combineLatest([ - this.filters$.pipe( - distinctUntilChanged( - (previousFilter, currentFilter) => - // Only update the collections when the organizationId filter changes - previousFilter.organization?.id === currentFilter.organization?.id, + folders$: Observable[]> = this.activeUserId$.pipe( + switchMap((userId) => + combineLatest([ + this.filters$.pipe( + distinctUntilChanged( + (previousFilter, currentFilter) => + // Only update the collections when the organizationId filter changes + previousFilter.organization?.id === currentFilter.organization?.id, + ), + ), + this.folderService.folderViews$(userId), + this.cipherViews$, + ]).pipe( + map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => { + if (folders.length === 1 && folders[0].id === null) { + // Do not display folder selections when only the "no folder" option is available. + return [filters, [], cipherViews]; + } + + // Sort folders by alphabetic name + folders.sort(Utils.getSortFunction(this.i18nService, "name")); + let arrangedFolders = folders; + + const noFolder = folders.find((f) => f.id === null); + + if (noFolder) { + // Update `name` of the "no folder" option to "Items with no folder" + const updatedNoFolder = { + ...noFolder, + name: this.i18nService.t("itemsWithNoFolder"), + }; + + // Move the "no folder" option to the end of the list + arrangedFolders = [...folders.filter((f) => f.id !== null), updatedNoFolder]; + } + return [filters, arrangedFolders, cipherViews]; + }), + map(([filters, folders, cipherViews]) => { + const organizationId = filters.organization?.id ?? null; + + // When no org or "My vault" is selected, return all folders + if (organizationId === null || organizationId === MY_VAULT_ID) { + return folders; + } + + const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId); + + // Return only the folders that have ciphers within the filtered organization + return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id)); + }), + map((folders) => { + const nestedFolders = this.getAllFoldersNested(folders); + return new DynamicTreeNode({ + fullList: folders, + nestedList: nestedFolders, + }); + }), + map((folders) => + folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")), + ), ), ), - this.folderService.folderViews$, - this.cipherViews$, - ]).pipe( - map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => { - if (folders.length === 1 && folders[0].id === null) { - // Do not display folder selections when only the "no folder" option is available. - return [filters, [], cipherViews]; - } - - // Sort folders by alphabetic name - folders.sort(Utils.getSortFunction(this.i18nService, "name")); - let arrangedFolders = folders; - - const noFolder = folders.find((f) => f.id === null); - - if (noFolder) { - // Update `name` of the "no folder" option to "Items with no folder" - noFolder.name = this.i18nService.t("itemsWithNoFolder"); - - // Move the "no folder" option to the end of the list - arrangedFolders = [...folders.filter((f) => f.id !== null), noFolder]; - } - return [filters, arrangedFolders, cipherViews]; - }), - map(([filters, folders, cipherViews]) => { - const organizationId = filters.organization?.id ?? null; - - // When no org or "My vault" is selected, return all folders - if (organizationId === null || organizationId === MY_VAULT_ID) { - return folders; - } - - const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId); - - // Return only the folders that have ciphers within the filtered organization - return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id)); - }), - map((folders) => { - const nestedFolders = this.getAllFoldersNested(folders); - return new DynamicTreeNode({ - fullList: folders, - nestedList: nestedFolders, - }); - }), - map((folders) => - folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")), - ), ); /** diff --git a/apps/browser/src/vault/popup/services/vault-popup-section.service.ts b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts new file mode 100644 index 00000000000..ed641e0cdf7 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-section.service.ts @@ -0,0 +1,129 @@ +import { computed, effect, inject, Injectable, signal, Signal } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; + +import { + KeyDefinition, + StateProvider, + VAULT_SETTINGS_DISK, +} from "@bitwarden/common/platform/state"; + +import { VaultPopupItemsService } from "./vault-popup-items.service"; + +export type PopupSectionOpen = { + favorites: boolean; + allItems: boolean; +}; + +const SECTION_OPEN_KEY = new KeyDefinition(VAULT_SETTINGS_DISK, "sectionOpen", { + deserializer: (obj) => obj, +}); + +const INITIAL_OPEN: PopupSectionOpen = { + favorites: true, + allItems: true, +}; + +@Injectable({ + providedIn: "root", +}) +export class VaultPopupSectionService { + private vaultPopupItemsService = inject(VaultPopupItemsService); + private stateProvider = inject(StateProvider); + + private hasFilterOrSearchApplied = toSignal( + this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => hasFilter)), + ); + + /** + * Used to change the open/close state without persisting it to the local disk. Reflects + * application-applied overrides. + * `null` means there is no current override + */ + private temporaryStateOverride = signal | null>(null); + + constructor() { + effect( + () => { + /** + * auto-open all sections when search or filter is applied, and remove + * override when search or filter is removed + */ + if (this.hasFilterOrSearchApplied()) { + this.temporaryStateOverride.set(INITIAL_OPEN); + } else { + this.temporaryStateOverride.set(null); + } + }, + { + allowSignalWrites: true, + }, + ); + } + + /** + * Stored disk state for the open/close state of the sections. Will be `null` if user has never + * opened/closed a section + */ + private sectionOpenStateProvider = this.stateProvider.getGlobal(SECTION_OPEN_KEY); + + /** + * Stored disk state for the open/close state of the sections, with an initial value provided + * if the stored disk state does not yet exist. + */ + private sectionOpenStoredState = toSignal( + this.sectionOpenStateProvider.state$.pipe(map((sectionOpen) => sectionOpen ?? INITIAL_OPEN)), + // Indicates that the state value is loading + { initialValue: null }, + ); + + /** + * Indicates the current open/close display state of each section, accounting for temporary + * non-persisted overrides. + */ + sectionOpenDisplayState: Signal> = computed(() => ({ + ...this.sectionOpenStoredState(), + ...this.temporaryStateOverride(), + })); + + /** + * Retrieve the open/close display state for a given section. + * + * @param sectionKey section key + */ + getOpenDisplayStateForSection(sectionKey: keyof PopupSectionOpen): Signal { + return computed(() => this.sectionOpenDisplayState()?.[sectionKey]); + } + + /** + * Updates the stored open/close state of a given section. Should be called only when a user action + * is taken directly to change the open/close state. + * + * Removes any current temporary override for the given section, as direct user action should + * supersede any application-applied overrides. + * + * @param sectionKey section key + */ + async updateSectionOpenStoredState( + sectionKey: keyof PopupSectionOpen, + open: boolean, + ): Promise { + await this.sectionOpenStateProvider.update((currentState) => { + return { + ...(currentState ?? INITIAL_OPEN), + [sectionKey]: open, + }; + }); + + this.temporaryStateOverride.update((prev) => { + if (prev !== null) { + return { + ...prev, + [sectionKey]: open, + }; + } + + return prev; + }); + } +} diff --git a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts deleted file mode 100644 index f50d6ebc236..00000000000 --- a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Injectable } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { - GlobalState, - KeyDefinition, - StateProvider, - VAULT_BROWSER_UI_ONBOARDING, -} from "@bitwarden/common/platform/state"; -import { DialogService } from "@bitwarden/components"; - -import { VaultUiOnboardingComponent } from "../components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component"; - -// Key definition for the Vault UI onboarding state. -// This key is used to store the state of the new UI information dialog. -export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition( - VAULT_BROWSER_UI_ONBOARDING, - "dialogState", - { - deserializer: (obj) => obj, - }, -); - -@Injectable() -export class VaultUiOnboardingService { - private onboardingUiReleaseDate = new Date("2024-12-10"); - - private vaultUiOnboardingState: GlobalState = this.stateProvider.getGlobal( - GLOBAL_VAULT_UI_ONBOARDING, - ); - - private readonly vaultUiOnboardingState$ = this.vaultUiOnboardingState.state$.pipe( - map((x) => x ?? false), - ); - - constructor( - private stateProvider: StateProvider, - private dialogService: DialogService, - private apiService: ApiService, - ) {} - - /** - * Checks whether the onboarding dialog should be shown and opens it if necessary. - * The dialog is shown if the user has not previously viewed it and is not a new account. - */ - async showOnboardingDialog(): Promise { - const hasViewedDialog = await this.getVaultUiOnboardingState(); - - if (!hasViewedDialog && !(await this.isNewAccount())) { - await this.openVaultUiOnboardingDialog(); - } - } - - private async openVaultUiOnboardingDialog(): Promise { - const dialogRef = VaultUiOnboardingComponent.open(this.dialogService); - - const result = firstValueFrom(dialogRef.closed); - - // Update the onboarding state when the dialog is closed - await this.setVaultUiOnboardingState(true); - - return result; - } - - private async isNewAccount(): Promise { - const userProfile = await this.apiService.getProfile(); - const profileCreationDate = new Date(userProfile.creationDate); - return profileCreationDate > this.onboardingUiReleaseDate; - } - - /** - * Updates and saves the state indicating whether the user has viewed - * the new UI onboarding information dialog. - */ - private async setVaultUiOnboardingState(value: boolean): Promise { - await this.vaultUiOnboardingState.update(() => value); - } - - /** - * Retrieves the current state indicating whether the user has viewed - * the new UI onboarding information dialog.s - */ - private async getVaultUiOnboardingState(): Promise { - return await firstValueFrom(this.vaultUiOnboardingState$); - } -} diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts index 38fbe659083..bca83a2fba0 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts @@ -16,7 +16,7 @@ import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-stat import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -import { PopupWidthService } from "../../../platform/popup/layout/popup-width.service"; +import { PopupSizeService } from "../../../platform/popup/layout/popup-size.service"; import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service"; import { AppearanceV2Component } from "./appearance-v2.component"; @@ -55,7 +55,7 @@ describe("AppearanceV2Component", () => { const setEnableCompactMode = jest.fn().mockResolvedValue(undefined); const setShowQuickCopyActions = jest.fn().mockResolvedValue(undefined); - const mockWidthService: Partial = { + const mockWidthService: Partial = { width$: new BehaviorSubject("default"), setWidth: jest.fn().mockResolvedValue(undefined), }; @@ -95,7 +95,7 @@ describe("AppearanceV2Component", () => { } as Partial, }, { - provide: PopupWidthService, + provide: PopupSizeService, useValue: mockWidthService, }, ], diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index c9e616b8e8e..3aab9f935e4 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -16,8 +16,14 @@ import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { BadgeModule, CheckboxModule, Option } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CardComponent } from "../../../../../../libs/components/src/card/card.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SelectModule } from "../../../../../../libs/components/src/select/select.module"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupCompactModeService } from "../../../platform/popup/layout/popup-compact-mode.service"; @@ -25,8 +31,8 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupWidthOption, - PopupWidthService, -} from "../../../platform/popup/layout/popup-width.service"; + PopupSizeService, +} from "../../../platform/popup/layout/popup-size.service"; import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-buttons.service"; @Component({ @@ -49,7 +55,7 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto export class AppearanceV2Component implements OnInit { private compactModeService = inject(PopupCompactModeService); private copyButtonsService = inject(VaultPopupCopyButtonsService); - private popupWidthService = inject(PopupWidthService); + private popupSizeService = inject(PopupSizeService); private i18nService = inject(I18nService); appearanceForm = this.formBuilder.group({ @@ -103,7 +109,7 @@ export class AppearanceV2Component implements OnInit { const showQuickCopyActions = await firstValueFrom( this.copyButtonsService.showQuickCopyActions$, ); - const width = await firstValueFrom(this.popupWidthService.width$); + const width = await firstValueFrom(this.popupSizeService.width$); // Set initial values for the form this.appearanceForm.setValue({ @@ -187,6 +193,6 @@ export class AppearanceV2Component implements OnInit { } async updateWidth(width: PopupWidthOption) { - await this.popupWidthService.setWidth(width); + await this.popupSizeService.setWidth(width); } } diff --git a/apps/browser/src/vault/popup/settings/appearance.component.html b/apps/browser/src/vault/popup/settings/appearance.component.html deleted file mode 100644 index a431fc72a1f..00000000000 --- a/apps/browser/src/vault/popup/settings/appearance.component.html +++ /dev/null @@ -1,80 +0,0 @@ -
    -
    - -
    -

    - {{ "appearance" | i18n }} -

    -
    - -
    -
    -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    - - -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/settings/appearance.component.ts b/apps/browser/src/vault/popup/settings/appearance.component.ts deleted file mode 100644 index e6d03c5b01f..00000000000 --- a/apps/browser/src/vault/popup/settings/appearance.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { ThemeType } from "@bitwarden/common/platform/enums"; -import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; - -import { enableAccountSwitching } from "../../../platform/flags"; - -@Component({ - selector: "vault-appearance", - templateUrl: "appearance.component.html", -}) -export class AppearanceComponent implements OnInit { - enableFavicon = false; - enableBadgeCounter = true; - theme: ThemeType; - themeOptions: any[]; - accountSwitcherEnabled = false; - enableRoutingAnimation: boolean; - - constructor( - private messagingService: MessagingService, - private domainSettingsService: DomainSettingsService, - private badgeSettingsService: BadgeSettingsServiceAbstraction, - i18nService: I18nService, - private themeStateService: ThemeStateService, - private animationControlService: AnimationControlService, - ) { - this.themeOptions = [ - { name: i18nService.t("default"), value: ThemeType.System }, - { name: i18nService.t("light"), value: ThemeType.Light }, - { name: i18nService.t("dark"), value: ThemeType.Dark }, - { name: "Nord", value: ThemeType.Nord }, - { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, - ]; - - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - this.enableRoutingAnimation = await firstValueFrom( - this.animationControlService.enableRoutingAnimation$, - ); - - this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$); - - this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$); - - this.theme = await firstValueFrom(this.themeStateService.selectedTheme$); - } - - async updateRoutingAnimation() { - await this.animationControlService.setEnableRoutingAnimation(this.enableRoutingAnimation); - } - - async updateFavicon() { - await this.domainSettingsService.setShowFavicons(this.enableFavicon); - } - - async updateBadgeCounter() { - await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter); - this.messagingService.send("bgUpdateContextMenu"); - } - - async saveTheme() { - await this.themeStateService.setSelectedTheme(this.theme); - } -} diff --git a/apps/browser/src/vault/popup/settings/folder-add-edit.component.html b/apps/browser/src/vault/popup/settings/folder-add-edit.component.html deleted file mode 100644 index 14393b83ddc..00000000000 --- a/apps/browser/src/vault/popup/settings/folder-add-edit.component.html +++ /dev/null @@ -1,49 +0,0 @@ -
    -
    -
    - -
    -

    - {{ title }} -

    -
    - -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - -
    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/settings/folder-add-edit.component.ts b/apps/browser/src/vault/popup/settings/folder-add-edit.component.ts deleted file mode 100644 index 122922a4d2d..00000000000 --- a/apps/browser/src/vault/popup/settings/folder-add-edit.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; - -import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { DialogService } from "@bitwarden/components"; -import { KeyService } from "@bitwarden/key-management"; - -@Component({ - selector: "app-folder-add-edit", - templateUrl: "folder-add-edit.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class FolderAddEditComponent extends BaseFolderAddEditComponent implements OnInit { - constructor( - folderService: FolderService, - folderApiService: FolderApiServiceAbstraction, - accountService: AccountService, - keyService: KeyService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - private router: Router, - private route: ActivatedRoute, - logService: LogService, - dialogService: DialogService, - formBuilder: FormBuilder, - ) { - super( - folderService, - folderApiService, - accountService, - keyService, - i18nService, - platformUtilsService, - logService, - dialogService, - formBuilder, - ); - } - - async ngOnInit() { - // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe - this.route.queryParams.pipe(first()).subscribe(async (params) => { - if (params.folderId) { - this.folderId = params.folderId; - } - await this.init(); - }); - } - - async submit(): Promise { - if (await super.submit()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/folders"]); - return true; - } - - return false; - } - - async delete(): Promise { - const confirmed = await super.delete(); - if (confirmed) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/folders"]); - } - return confirmed; - } -} diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts index eecad04613e..9c202e26fef 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts @@ -4,10 +4,13 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { DialogService } from "@bitwarden/components"; @@ -52,8 +55,9 @@ describe("FoldersV2Component", () => { { provide: PlatformUtilsService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, { provide: LogService, useValue: mock() }, - { provide: FolderService, useValue: { folderViews$ } }, + { provide: FolderService, useValue: { folderViews$: () => folderViews$ } }, { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, ], }) .overrideComponent(FoldersV2Component, { diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.ts index ce196132f88..8abc3f906c0 100644 --- a/apps/browser/src/vault/popup/settings/folders-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/folders-v2.component.ts @@ -1,8 +1,10 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { map, Observable } from "rxjs"; +import { filter, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { @@ -13,8 +15,14 @@ import { } from "@bitwarden/components"; import { VaultIcons } from "@bitwarden/vault"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ItemModule } from "../../../../../../libs/components/src/item/item.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no-items.module"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -45,18 +53,21 @@ export class FoldersV2Component { folders$: Observable; NoFoldersIcon = VaultIcons.NoFolders; + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); constructor( private folderService: FolderService, private dialogService: DialogService, + private accountService: AccountService, ) { - this.folders$ = this.folderService.folderViews$.pipe( + this.folders$ = this.activeUserId$.pipe( + filter((userId): userId is UserId => userId !== null), + switchMap((userId) => this.folderService.folderViews$(userId)), map((folders) => { // Remove the last folder, which is the "no folder" option folder if (folders.length > 0) { return folders.slice(0, folders.length - 1); } - return folders; }), ); diff --git a/apps/browser/src/vault/popup/settings/folders.component.html b/apps/browser/src/vault/popup/settings/folders.component.html deleted file mode 100644 index 47cdb0188d2..00000000000 --- a/apps/browser/src/vault/popup/settings/folders.component.html +++ /dev/null @@ -1,38 +0,0 @@ -
    -
    - -
    -

    - {{ "folders" | i18n }} -

    -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    - -
    -

    {{ "noFolders" | i18n }}

    -
    -
    -
    diff --git a/apps/browser/src/vault/popup/settings/folders.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts deleted file mode 100644 index edf7fe939e8..00000000000 --- a/apps/browser/src/vault/popup/settings/folders.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component } from "@angular/core"; -import { Router } from "@angular/router"; -import { map, Observable } from "rxjs"; - -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; - -@Component({ - selector: "app-folders", - templateUrl: "folders.component.html", -}) -export class FoldersComponent { - folders$: Observable; - - constructor( - private folderService: FolderService, - private router: Router, - ) { - this.folders$ = this.folderService.folderViews$.pipe( - map((folders) => { - if (folders.length > 0) { - folders = folders.slice(0, folders.length - 1); - } - - return folders; - }), - ); - } - - folderSelected(folder: FolderView) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/edit-folder"], { queryParams: { folderId: folder.id } }); - } - - addFolder() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-folder"]); - } -} diff --git a/apps/browser/src/vault/popup/settings/sync.component.html b/apps/browser/src/vault/popup/settings/sync.component.html deleted file mode 100644 index 6d0a1c31a8b..00000000000 --- a/apps/browser/src/vault/popup/settings/sync.component.html +++ /dev/null @@ -1,35 +0,0 @@ -
    -
    - -
    -

    - {{ "sync" | i18n }} -

    -
    -
    -
    -
    - -

    - {{ "lastSync" | i18n }} {{ lastSync }} -

    -
    -
    diff --git a/apps/browser/src/vault/popup/settings/sync.component.ts b/apps/browser/src/vault/popup/settings/sync.component.ts deleted file mode 100644 index 6585a71d94b..00000000000 --- a/apps/browser/src/vault/popup/settings/sync.component.ts +++ /dev/null @@ -1,46 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; - -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; - -@Component({ - selector: "app-sync", - templateUrl: "sync.component.html", -}) -export class SyncComponent implements OnInit { - lastSync = "--"; - syncPromise: Promise; - - constructor( - private syncService: SyncService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - ) {} - - async ngOnInit() { - await this.setLastSync(); - } - - async sync() { - this.syncPromise = this.syncService.fullSync(true); - const success = await this.syncPromise; - if (success) { - await this.setLastSync(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingComplete")); - } else { - this.platformUtilsService.showToast("error", null, this.i18nService.t("syncingFailed")); - } - } - - async setLastSync() { - const last = await this.syncService.getLastSync(); - if (last != null) { - this.lastSync = last.toLocaleDateString() + " " + last.toLocaleTimeString(); - } else { - this.lastSync = this.i18nService.t("never"); - } - } -} diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html index a79b6c74b03..c77ac4f9755 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html @@ -13,10 +13,25 @@ [appA11yTitle]="'viewItemTitle' | i18n: cipher.name" (click)="onViewCipher(cipher)" > - +
    + +
    {{ cipher.name }} + + + {{ cipher.subTitle }} - + - - -

    - {{ "vault" | i18n }} -

    -
    - -
    - -
    -
    -
    - - - - -
    -
    -
    diff --git a/apps/browser/src/vault/popup/settings/vault-settings.component.ts b/apps/browser/src/vault/popup/settings/vault-settings.component.ts deleted file mode 100644 index a12f6d1d5be..00000000000 --- a/apps/browser/src/vault/popup/settings/vault-settings.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Component } from "@angular/core"; -import { Router } from "@angular/router"; - -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; - -@Component({ - selector: "vault-settings", - templateUrl: "vault-settings.component.html", -}) -export class VaultSettingsComponent { - constructor( - public messagingService: MessagingService, - private router: Router, - ) {} - - async import() { - await this.router.navigate(["/import"]); - if (await BrowserApi.isPopupOpen()) { - await BrowserPopupUtils.openCurrentPagePopout(window); - } - } -} diff --git a/apps/browser/src/vault/services/vault-filter.service.ts b/apps/browser/src/vault/services/vault-filter.service.ts index ba1d11f5d27..305c7de487b 100644 --- a/apps/browser/src/vault/services/vault-filter.service.ts +++ b/apps/browser/src/vault/services/vault-filter.service.ts @@ -24,7 +24,7 @@ export class VaultFilterService extends BaseVaultFilterService { collectionService: CollectionService, policyService: PolicyService, stateProvider: StateProvider, - private accountService: AccountService, + accountService: AccountService, ) { super( organizationService, @@ -33,6 +33,7 @@ export class VaultFilterService extends BaseVaultFilterService { collectionService, policyService, stateProvider, + accountService, ); this.vaultFilter.myVaultOnly = false; this.vaultFilter.selectedOrganizationId = null; diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index 4292ebc4b31..feff0fc99e2 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -118,10 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Xestor de contrasinais + Xestor de Contrasinais Bitwarden - En casa, no traballo ou mentres estás a viaxar, Bitwarden protexe doadamente todos os teus contrasinais, chaves de paso, e información sensíbel. + En casa, no traballo ou en movemento, Bitwarden protexe doadamente todos os teus contrasinais, chaves de paso, e información sensíbel. Recoñecido como o mellor xestor de contrasinais por PCMag, WIRED, The Verge, CNET, G2 e moito máis. @@ -181,7 +181,7 @@ As solucións de xestión de credenciais cifradas de extremo a extremo de Bitwar Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites - A túa caixa forte tamén é facilmente accesible desde o menú de clic dereito + A túa caixa forte tamén é facilmente accesible dende o menú de clic dereito Xera automaticamente contrasinais fortes, aleatorias e seguras diff --git a/apps/browser/store/locales/nl/copy.resx b/apps/browser/store/locales/nl/copy.resx index 6e646d5ece8..17720f54f4c 100644 --- a/apps/browser/store/locales/nl/copy.resx +++ b/apps/browser/store/locales/nl/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - wachtwoordbeheerder + Bitwarden Wachtwoordbeheerder - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Thuis, op het werk of onderweg, Bitwarden beveiligt eenvoudig al je wachtwoorden, sleutels en gevoelige informatie. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Erkend als de beste wachtwoordmanager door PCMag, WIRED, The Verge, CNET, G2 en meer! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +BEVEILIG JE DIGITALE LEVEN +Beveilig je digitale leven en bescherm je tegen datalekken door unieke, sterke wachtwoorden te genereren en op te slaan voor elke account. Bewaar alles in een end-to-end versleutelde wachtwoordkluis waar alleen jij toegang toe hebt. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +OVERAL EN ALTIJD TOEGANG TOT JE GEGEVENS, OP ELK APPARAAT +Beheer, bewaar, beveilig en deel een onbeperkt aantal wachtwoorden op een onbeperkt aantal apparaten zonder beperkingen. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +IEDEREEN ZOU DE MIDDELEN MOETEN HEBBEN OM VEILIG ONLINE TE BLIJVEN +Gebruik Bitwarden gratis, zonder advertenties of verkoop van gegevens. Bitwarden vindt dat iedereen de mogelijkheid moet hebben om veilig online te zijn. Premium abonnementen bieden toegang tot geavanceerde functies. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +VERSTERK JE TEAMS MET BITWARDEN +Abonnementen voor Teams en Enterprise worden geleverd met professionele zakelijke functies. Enkele voorbeelden zijn SSO-integratie, zelf hosten, directory-integratie en SCIM provisioning, globaal beleid, API-toegang, gebeurtenislogboeken en meer. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Gebruik Bitwarden om je medewerkers te beveiligen en gevoelige informatie te delen met collega's. -More reasons to choose Bitwarden: +Meer redenen om voor Bitwarden te kiezen: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Encryptie van wereldklasse +Wachtwoorden worden beschermd met geavanceerde end-to-end versleuteling (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat je gegevens veilig en privé blijven. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Audits door derde partijen +Bitwarden voert regelmatig uitgebreide beveiligingsaudits uit bij gerenommeerde beveiligingsbedrijven. Deze jaarlijkse audits omvatten broncodebeoordelingen en penetratietests voor Bitwarden IP's, servers en webapplicaties. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Geavanceerde 2FA +Beveilig je login met een authenticator van derden, codes per e-mail of FIDO2 WebAuthn referenties zoals een hardware beveiligingssleutel of passkey. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Verstuur gegevens rechtstreeks naar anderen met behoud van end-to-end versleutelde beveiliging en beperking van blootstelling. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Ingebouwde generator +Maak lange, complexe en duidelijke wachtwoorden en unieke gebruikersnamen voor elke site die je bezoekt. Integreer met e-mail alias providers voor extra privacy. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Wereldwijde vertalingen +Bitwarden vertalingen bestaan voor meer dan 60 talen, vertaald door de wereldwijde gemeenschap via Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Platformoverkoepelende applicaties +Beveilig en deel gevoelige gegevens in je Bitwarden Vault vanuit elke browser, mobiel apparaat of desktop OS, en meer. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden beveiligt meer dan alleen wachtwoorden +Met de end-to-end versleutelde oplossingen voor referentiebeheer van Bitwarden kunnen organisaties alles beveiligen, inclusief ontwikkelaarsgeheimen en ervaringen met wachtwoorden. Bezoek Bitwarden.com voor meer informatie over Bitwarden Secrets Manager en Bitwarden Passwordless.dev! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Thuis, op het werk of onderweg, Bitwarden beveiligt eenvoudig al je wachtwoorden, sleutels en gevoelige informatie. Synchroniseer en gebruik je kluis op meerdere apparaten diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 24331b3a1ba..f4643115237 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -121,52 +121,54 @@ Gerenciador de Senhas Bitwarden - Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. + Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, chaves de acesso e informações sensíveis. Reconhecido como o melhor gerenciador de senhas por "PCMag", 'WIRED', 'The Verge', 'CNET', 'G2', entre outros! PROTEJA A SUA VIDA DIGITAL -Deixe a sua vida digital segura e se proteja contra violações de dados gerando e salvando senhas únicas e fortes para cada conta pessoal. Mantendo tudo isso em um cofre encriptografado de ponta a ponta que apenas você pode acessar. +Deixe a sua vida digital segura e se proteja contra violações de dados gerando e salvando senhas únicas e fortes para cada conta pessoal. Mantendo tudo isso em um cofre criptografado de ponta a ponta em que apenas você pode acessar. ACESSE OS SEUS DADOS, EM QUALQUER LUGAR, HORA E DISPOSITIVO Gerencie, armazene, proteja e compartilhe senhas facilmente entre dispositivos ilimitados e sem restrições. TODOS DEVERIAM TEM FERRAMENTAS PARA SE PROTEGER ONLINE -Utilize Bitwarden de graça sem anuncios ou venda dos seus dados. A Bitwarden acredita que todos deveriam ter a opção de estar seguro online. Planos Premium oferecem recursos mais avançados. +Utilize Bitwarden de graça sem anúncios ou venda dos seus dados. A Bitwarden acredita que todos deveriam ter a opção de estar seguro online. Nossos planos Premium oferecem recursos mais avançados. EMPODERE O SEUS TIMES COM BITWARDEN -Os Planos para times e empresas vem com recursos profissionais para negócios. Alguns exemplos inculuem integração SSO, Auto-hospedagem, integração de diretório e provisionamento SCIM, políticas globais, acesso API, registro de eventos e mais. +Os planos para times e empresas vêm com recursos profissionais para negócios. Alguns exemplos incluem integração SSO, auto-hospedagem, integração de diretório e provisionamento SCIM, políticas globais, acesso API, registro de eventos e mais. -Utilizer Bitwarn para proteger os seus empregados e compartilhar informações sensíveis para os colegas. +Utilize o Bitwarden para proteger os seus colaboradores e compartilhar informações sensíveis com seus colegas. Mais razões para escolher Bitwarden: -Senhas encriptografadas por classe de palavras são protegidas com criptografia de ponta a ponta avançada (AES-256 bit, salted hashtag, and PBKDF2 SHA-256), então os seus dados ficam seguros e privados. +Criptografia mundialmente reconhecida +Suas senhas são protegidas com avançada criptografia ponta a ponta (AES-256, salted hashtag, e PBKDF2 SHA-256) para que os seus dados estejam seguros e privados. Auditoria de Terceiros -Bitwarden regularmente conduz auditorias de terceiros com notáveis empresas de segurança. Essas audições anuais incluem qualificação e penetração do código fonte através de IPs da Bitwarden, servidores e aplicações WEB. +A Bitwarden regularmente conduz auditorias de terceiros com notáveis empresas de segurança. Essas auditorias anuais incluem qualificação do código fonte e testes de invasão contra os IPs da Bitwarden, servidores e aplicações web. 2FA Avançado -Proteja o seu login com um autenticador de doisfatores, códigos de email ou credenciais FIDO2 WebAuthn como chave de segurança por hardware ou chave de acesso. +Proteja o seu login com um autenticador de dois fatores, códigos de email ou credenciais FIDO2 WebAuthn como chave de segurança por hardware ou chave de acesso. Envio Bitwarden -Transmita dados diretamente para outros enquanto mantem segurança de encriptografia ponta a ponta e limitando exposição. +Transmita dados diretamente para outras pessoas enquanto mantém a segurança da criptografia ponta a ponta para limitar sua exposição. Gerador integrado -Crie senhas grandes, complexas e diferentes e nomes de usuário unicos para cada site que visitar. Integre com provedores de email para privacidade adicional +Crie diferentes senhas fortes e complexas e nomes de usuário únicos para cada site que visitar. Integre com provedores de email para privacidade adicional. Tradutores globais -As traduções da Bitwarden estão disponíveis para mais de 60 líguas, traduzidas pela comunidade global através do Crowdin. +As traduções da Bitwarden estão disponíveis para mais de 60 línguas, traduzidas pela comunidade global através do Crowdin. Aplicações Multi-Plataforma -Proteja e compartilhe conteúdo sensível dentro do Vault da Bitwarden de qualquer navegador, dispositivo móvel, ou desktop OS, entre outros. +Proteja e compartilhe conteúdo confidencial dentro do Vault da Bitwarden de qualquer navegador, dispositivo móvel, desktop, entre outros. -Bitwarden protege mais do que apenas soluções em gerenciamento de credenciais de senhas encriptografadas ponta a ponta, Bitwarden empodera organizações para proteger qualquer coisa, incluindo segredos de desenvolvedor e chaves de acesso. -Visite Bitwarden.com para aprender mais sobre Bitwarden Secrets Manager e Bitwarden Passwordless.dev! +Bitwarden protege mais do que apenas senhas +Com nossas soluções em gerenciamento de credenciais criptografadas ponta a ponta, a Bitwarden empodera organizações para proteger qualquer informação, incluindo senhas de desenvolvedor e chaves de acesso. Visite Bitwarden.com para aprender mais sobre Bitwarden Secrets Manager e Bitwarden Passwordless.dev! + - Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. + Em casa, no trabalho ou em qualquer lugar, a Bitwarden protege facilmente todas as suas senhas, chaves de acesso e informações sensíveis. Sincronize e acesse o seu cofre através de múltiplos dispositivos diff --git a/apps/browser/store/locales/ru/copy.resx b/apps/browser/store/locales/ru/copy.resx index 7c9480567e8..0da1e70f897 100644 --- a/apps/browser/store/locales/ru/copy.resx +++ b/apps/browser/store/locales/ru/copy.resx @@ -124,48 +124,47 @@ Дома, на работе или в пути - Bitwarden всегда защитит ваши пароли, passkeys и конфиденциальную информацию. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Признан лучшим менеджером паролей по версии PCMag, WIRED, The Verge, CNET, G2 и других! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +ЗАЩИТИТЕ СВОЮ ЦИФРОВУЮ ЖИЗНЬ +Защитите свою цифровую жизнь и защитите её от утечек данных, создавая уникальные, надёжные пароли для каждой учетной записи. Сохраните всё в зашифрованном сквозным шифрованием хранилище паролей, доступ к которому есть только у вас. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ДОСТУП К СВОИМ ДАННЫМ В ЛЮБОМ МЕСТЕ, В ЛЮБОЕ ВРЕМЯ, НА ЛЮБОМ УСТРОЙСТВЕ +Легко управляйте, храните, защищайте и делитесь неограниченным количеством паролей на неограниченном количестве устройств без ограничений. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +КАЖДЫЙ ДОЛЖЕН ИМЕТЬ ИНСТРУМЕНТЫ ДЛЯ БЕЗОПАСНОСТИ В СЕТИ +Используйте Bitwarden бесплатно без рекламы или продажи данных. Bitwarden считает, что каждый должен иметь возможность оставаться в безопасности в сети. Премиум-планы предлагают доступ к расширенным функциям. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +РАСШИРЯЙТЕ ВОЗМОЖНОСТИ СВОИХ КОМАНД С ПОМОЩЬЮ BITWARDEN +Планы для Teams и Enterprise включают профессиональные бизнес-функции. Вот несколько примеров: интеграция SSO, собственный хостинг, интеграция каталогов и SCIM, глобальные политики, доступ через API, журналы событий и многое другое. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Используйте Bitwarden для защиты своих сотрудников и обмена конфиденциальной информацией с коллегами. +Дополнительные причины выбрать Bitwarden: -More reasons to choose Bitwarden: +Шифрование мирового класса +Пароли защищены усовершенствованным сквозным шифрованием (AES-256, использование salt и PBKDF2 SHA-256), поэтому ваши данные остаются в безопасности и конфиденциальности. -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Сторонние аудиты +Bitwarden регулярно проводит комплексные сторонние аудиты безопасности с известными фирмами по безопасности. Эти ежегодные аудиты включают оценку исходного кода и тестирование на проникновение по IP-адресам Bitwarden, серверам и веб-приложениям. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Расширенная 2FA +Защитите свой вход с помощью стороннего аутентификатора, кодов, отправленных по электронной почте, или учетных данных FIDO2 WebAuthn, таких как аппаратный ключ безопасности или ключ доступа. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Передавайте данные другим без посредников, сохраняя сквозное шифрование и ограничивая раскрытие информации. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Встроенный генератор паролей +Создавайте длинные, сложные и уникальные пароли и имена пользователей для каждого посещаемого вами сайта. Интегрируйтесь с поставщиками псевдонимов электронной почты для дополнительной конфиденциальности. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Многоязычный перевод +Bitwarden переведён на более чем 60 языков, с помощью мирового сообщества через Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Кроссплатформенные приложения +Защищайте и делитесь конфиденциальными данными в вашем хранилище Bitwarden из любого браузера, мобильного устройства или настольной ОС и т. д. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden защищает не только пароли +Решения со сквозным шифрованием для управления учётными данными от Bitwarden позволяют организациям защищать всё, включая секреты разработчиков и ключи доступа. Посетите Bitwarden.com, чтобы узнать больше о Bitwarden Secrets Manager и Bitwarden Passwordless.dev! diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index ea98321a499..0f73ccd6619 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -121,7 +121,7 @@ Bitwarden 密码管理器 - 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 + 无论是在家中、工作中还是在旅途中,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 被 PCMag、WIRED、The Verge、CNET、G2 等评为最佳的密码管理器! @@ -135,11 +135,12 @@ 每个人都应该拥有的保持在线安全的工具 使用 Bitwarden 是免费的,没有广告,不会出售数据。Bitwarden 相信每个人都应该拥有保持在线安全的能力。高级计划提供了对高级功能的访问。 -使用 BITWARDEN 为您的团队提供支持 +使用 Bitwarden 为您的团队提供支持 团队计划和企业计划具有专业的商业功能。例如 SSO 集成、自托管、目录集成,以及 SCIM 配置、全局策略、API 访问、事件日志等。 使用 Bitwarden 保护您的劳动成果,并与同事共享敏感信息。 + 选择 Bitwarden 的更多理由: 世界级加密 @@ -164,10 +165,10 @@ Bitwarden 的翻译涵盖 60 多种语言,由全球社区使用 Crowdin 翻译 从任何浏览器、移动设备或桌面操作系统中安全地访问和共享 Bitwarden 密码库中的敏感数据。 Bitwarden 保护的不仅仅是密码 -Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 了解更多关于 Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev 的信息! +Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 进一步了解 Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev! - 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 + 无论是在家中、工作中还是在旅途中,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 在多个设备间同步和访问您的密码库 diff --git a/apps/browser/tailwind.config.js b/apps/browser/tailwind.config.js index 2e8f9c9f817..d0ec8025c66 100644 --- a/apps/browser/tailwind.config.js +++ b/apps/browser/tailwind.config.js @@ -5,6 +5,7 @@ config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", + "../../libs/key-management/src/**/*.{html,ts}", "../../libs/vault/src/**/*.{html,ts}", "../../libs/angular/src/**/*.{html,ts}", "../../libs/vault/src/**/*.{html,ts}", diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 6b53186e076..2326e68f55d 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -24,16 +24,18 @@ "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], + "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer/ui": ["../../libs/importer/src/components"], + "@bitwarden/key-management": ["../../libs/key-management/src"], + "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], + "@bitwarden/platform": ["../../libs/platform/src"], + "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], + "@bitwarden/tools-card": ["../../libs/tools/card/src"], + "@bitwarden/ui-common": ["../../libs/ui/common/src"], "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], - "@bitwarden/importer/core": ["../../libs/importer/src"], - "@bitwarden/importer/ui": ["../../libs/importer/src/components"], - "@bitwarden/platform": ["../../libs/platform/src"], - "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], - "@bitwarden/tools-card": ["../../libs/tools/card/src"], - "@bitwarden/key-management": ["../../libs/key-management/src"], "@bitwarden/vault": ["../../libs/vault/src"] }, "plugins": [ diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 6ba74d7df43..bce41d64d1f 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -206,9 +206,7 @@ const mainConfig = { "overlay/list": "./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts", "encrypt-worker": "../../libs/common/src/platform/services/cryptography/encrypt.worker.ts", - "content/lp-fileless-importer": "./src/tools/content/lp-fileless-importer.ts", "content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts", - "content/lp-suppress-import-download": "./src/tools/content/lp-suppress-import-download.ts", }, optimization: { minimize: ENV !== "development", @@ -312,8 +310,6 @@ if (manifestVersion == 2) { // Manifest V2 background pages can be run through the regular build pipeline. // Since it's a standard webpage. mainConfig.entry.background = "./src/platform/background.ts"; - mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] = - "./src/tools/content/lp-suppress-import-download-script-append.mv2.ts"; mainConfig.entry["content/fido2-page-script-append-mv2"] = "./src/autofill/fido2/content/fido2-page-script-append.mv2.ts"; mainConfig.entry["content/fido2-page-script-delay-append-mv2"] = diff --git a/apps/cli/package.json b/apps/cli/package.json index b25f327c42a..d1d8ac76ec4 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.12.0", + "version": "2025.1.1", "keywords": [ "bitwarden", "password", @@ -63,7 +63,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "form-data": "4.0.0", + "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jsdom": "25.0.1", @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.69", + "tldts": "6.1.71", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts index 5b351efe459..e26d073326e 100644 --- a/apps/cli/src/admin-console/commands/share.command.ts +++ b/apps/cli/src/admin-console/commands/share.command.ts @@ -34,6 +34,8 @@ export class ShareCommand { if (req == null || req.length === 0) { return Response.badRequest("You must provide at least one collection id for this item."); } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.badRequest("Error parsing the encoded request data."); } diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 2a3d5d85408..359ed08ca99 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -165,6 +165,8 @@ export class LoginCommand { if (options.method != null) { twoFactorMethod = parseInt(options.method, null); } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.error("Invalid two-step login method."); } @@ -240,6 +242,8 @@ export class LoginCommand { if (twoFactorMethod != null) { try { selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0]; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.error("Invalid two-step login method."); } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index da7c98a35d6..13152f3d29e 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -14,6 +14,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { KeyService } from "@bitwarden/key-management"; import { OrganizationCollectionRequest } from "../admin-console/models/request/organization-collection.request"; @@ -24,6 +25,8 @@ import { CipherResponse } from "../vault/models/cipher.response"; import { FolderResponse } from "../vault/models/folder.response"; export class EditCommand { + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + constructor( private cipherService: CipherService, private folderService: FolderService, @@ -55,6 +58,8 @@ export class EditCommand { try { const reqJson = Buffer.from(requestJson, "base64").toString(); req = JSON.parse(reqJson); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.badRequest("Error parsing the encoded request data."); } @@ -121,12 +126,12 @@ export class EditCommand { cipher.collectionIds = req; try { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher); const decCipher = await updatedCipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), + await this.cipherService.getKeyForCipherKeyDecryption( + updatedCipher, + await firstValueFrom(this.activeUserId$), + ), ); const res = new CipherResponse(decCipher); return Response.success(res); @@ -136,7 +141,8 @@ export class EditCommand { } private async editFolder(id: string, req: FolderExport) { - const folder = await this.folderService.getFromState(id); + const activeUserId = await firstValueFrom(this.activeUserId$); + const folder = await this.folderService.getFromState(id, activeUserId); if (folder == null) { return Response.notFound(); } @@ -144,12 +150,11 @@ export class EditCommand { let folderView = await folder.decrypt(); folderView = FolderExport.toView(req, folderView); - const activeUserId = await firstValueFrom(this.accountService.activeAccount$); - const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); const encFolder = await this.folderService.encrypt(folderView, userKey); try { - await this.folderApiService.save(encFolder); - const updatedFolder = await this.folderService.get(folder.id); + const folder = await this.folderApiService.save(encFolder, activeUserId); + const updatedFolder = new Folder(folder); const decFolder = await updatedFolder.decrypt(); const res = new FolderResponse(decFolder); return Response.success(res); diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 1a835befd01..a1fec7a7472 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -51,6 +51,8 @@ import { FolderResponse } from "../vault/models/folder.response"; import { DownloadCommand } from "./download.command"; export class GetCommand extends DownloadCommand { + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + constructor( private cipherService: CipherService, private folderService: FolderService, @@ -113,10 +115,8 @@ export class GetCommand extends DownloadCommand { let decCipher: CipherView = null; if (Utils.isGuid(id)) { const cipher = await this.cipherService.get(id); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); if (cipher != null) { + const activeUserId = await firstValueFrom(this.activeUserId$); decCipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); @@ -152,11 +152,9 @@ export class GetCommand extends DownloadCommand { } } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.eventCollectionService.collect( + await this.eventCollectionService.collect( EventType.Cipher_ClientViewed, - id, + decCipher.id, true, decCipher.organizationId, ); @@ -262,8 +260,9 @@ export class GetCommand extends DownloadCommand { return Response.error("Couldn't generate TOTP code."); } + const account = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.accountProfileService.hasPremiumFromAnySource$, + this.accountProfileService.hasPremiumFromAnySource$(account.id), ); if (!canAccessPremium) { const originalCipher = await this.cipherService.get(cipher.id); @@ -347,8 +346,9 @@ export class GetCommand extends DownloadCommand { return Response.multipleResults(attachments.map((a) => a.id)); } + const account = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.accountProfileService.hasPremiumFromAnySource$, + this.accountProfileService.hasPremiumFromAnySource$(account.id), ); if (!canAccessPremium) { const originalCipher = await this.cipherService.get(cipher.id); @@ -383,13 +383,14 @@ export class GetCommand extends DownloadCommand { private async getFolder(id: string) { let decFolder: FolderView = null; + const activeUserId = await firstValueFrom(this.activeUserId$); if (Utils.isGuid(id)) { - const folder = await this.folderService.getFromState(id); + const folder = await this.folderService.getFromState(id, activeUserId); if (folder != null) { decFolder = await folder.decrypt(); } } else if (id.trim() !== "") { - let folders = await this.folderService.getAllDecryptedFromState(); + let folders = await this.folderService.getAllDecryptedFromState(activeUserId); folders = CliUtils.searchFolders(folders, id); if (folders.length > 1) { return Response.multipleResults(folders.map((f) => f.id)); @@ -551,9 +552,7 @@ export class GetCommand extends DownloadCommand { private async getFingerprint(id: string) { let fingerprint: string[] = null; if (id === "me") { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId)); fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey); } else if (Utils.isGuid(id)) { diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 9cb36f71496..92da86b696a 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { OrganizationUserApiService, @@ -12,6 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventType } from "@bitwarden/common/enums"; import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -38,6 +39,7 @@ export class ListCommand { private organizationUserApiService: OrganizationUserApiService, private apiService: ApiService, private eventCollectionService: EventCollectionService, + private accountService: AccountService, ) {} async run(object: string, cmdOptions: Record): Promise { @@ -135,7 +137,10 @@ export class ListCommand { } private async listFolders(options: Options) { - let folders = await this.folderService.getAllDecryptedFromState(); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + let folders = await this.folderService.getAllDecryptedFromState(activeUserId); if (options.search != null && options.search.trim() !== "") { folders = CliUtils.searchFolders(folders, options.search); diff --git a/apps/cli/src/key-management/cli-biometrics-service.ts b/apps/cli/src/key-management/cli-biometrics-service.ts new file mode 100644 index 00000000000..bda8fe82895 --- /dev/null +++ b/apps/cli/src/key-management/cli-biometrics-service.ts @@ -0,0 +1,27 @@ +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management"; + +export class CliBiometricsService extends BiometricsService { + async authenticateWithBiometrics(): Promise { + return false; + } + + async getBiometricsStatus(): Promise { + return BiometricsStatus.PlatformUnsupported; + } + + async unlockWithBiometricsForUser(userId: UserId): Promise { + return null; + } + + async getBiometricsStatusForUser(userId: UserId): Promise { + return BiometricsStatus.PlatformUnsupported; + } + + async getShouldAutopromptNow(): Promise { + return false; + } + + async setShouldAutopromptNow(value: boolean): Promise {} +} diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index 71c372ef301..be476d19814 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -76,6 +76,7 @@ export class OssServeConfigurator { this.serviceContainer.organizationUserApiService, this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, + this.serviceContainer.accountService, ); this.createCommand = new CreateCommand( this.serviceContainer.cipherService, @@ -115,6 +116,7 @@ export class OssServeConfigurator { this.serviceContainer.folderApiService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.cipherAuthorizationService, + this.serviceContainer.accountService, ); this.confirmCommand = new ConfirmCommand( this.serviceContainer.apiService, @@ -147,6 +149,7 @@ export class OssServeConfigurator { this.serviceContainer.environmentService, this.serviceContainer.sendApiService, this.serviceContainer.billingAccountProfileStateService, + this.serviceContainer.accountService, ); this.sendDeleteCommand = new SendDeleteCommand( this.serviceContainer.sendService, @@ -164,6 +167,7 @@ export class OssServeConfigurator { this.sendGetCommand, this.serviceContainer.sendApiService, this.serviceContainer.billingAccountProfileStateService, + this.serviceContainer.accountService, ); this.sendListCommand = new SendListCommand( this.serviceContainer.sendService, diff --git a/apps/cli/src/platform/flags.ts b/apps/cli/src/platform/flags.ts index dc0103e2436..a762053da35 100644 --- a/apps/cli/src/platform/flags.ts +++ b/apps/cli/src/platform/flags.ts @@ -7,11 +7,9 @@ import { } from "@bitwarden/common/platform/misc/flags"; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = {} & SharedFlags; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = {} & SharedDevFlags; export function flagEnabled(flag: keyof Flags): boolean { diff --git a/apps/cli/src/platform/services/node-env-secure-storage.service.ts b/apps/cli/src/platform/services/node-env-secure-storage.service.ts index 5e9fdd26b3c..2807509e428 100644 --- a/apps/cli/src/platform/services/node-env-secure-storage.service.ts +++ b/apps/cli/src/platform/services/node-env-secure-storage.service.ts @@ -87,6 +87,8 @@ export class NodeEnvSecureStorageService implements AbstractStorageService { } return Utils.fromBufferToB64(decValue); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.logService.info("Decrypt error."); return null; @@ -104,6 +106,8 @@ export class NodeEnvSecureStorageService implements AbstractStorageService { } } } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.logService.info("Session key is invalid."); } diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index df3a2a805bc..d6e325fe380 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -165,6 +165,7 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { CliBiometricsService } from "../key-management/cli-biometrics-service"; import { flagEnabled } from "../platform/flags"; import { CliPlatformUtilsService } from "../platform/services/cli-platform-utils.service"; import { ConsoleLogService } from "../platform/services/console-log.service"; @@ -483,7 +484,29 @@ export class ServiceContainer { this.containerService = new ContainerService(this.keyService, this.encryptService); - this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); + this.configApiService = new ConfigApiService(this.apiService, this.tokenService); + + this.authService = new AuthService( + this.accountService, + this.messagingService, + this.keyService, + this.apiService, + this.stateService, + this.tokenService, + ); + + this.configService = new DefaultConfigService( + this.configApiService, + this.environmentService, + this.logService, + this.stateProvider, + this.authService, + ); + + this.domainSettingsService = new DefaultDomainSettingsService( + this.stateProvider, + this.configService, + ); this.fileUploadService = new FileUploadService(this.logService, this.apiService); @@ -575,29 +598,12 @@ export class ServiceContainer { this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.stateProvider, + this.platformUtilsService, + this.apiService, ); this.taskSchedulerService = new DefaultTaskSchedulerService(this.logService); - this.authService = new AuthService( - this.accountService, - this.messagingService, - this.keyService, - this.apiService, - this.stateService, - this.tokenService, - ); - - this.configApiService = new ConfigApiService(this.apiService, this.tokenService); - - this.configService = new DefaultConfigService( - this.configApiService, - this.environmentService, - this.logService, - this.stateProvider, - this.authService, - ); - this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, @@ -688,12 +694,12 @@ export class ServiceContainer { this.userVerificationApiService, this.userDecryptionOptionsService, this.pinService, - this.logService, - this.vaultTimeoutSettingsService, - this.platformUtilsService, this.kdfConfigService, + new CliBiometricsService(), ); + const biometricService = new CliBiometricsService(); + this.vaultTimeoutService = new VaultTimeoutService( this.accountService, this.masterPasswordService, @@ -709,6 +715,7 @@ export class ServiceContainer { this.stateEventRunnerService, this.taskSchedulerService, this.logService, + biometricService, lockedCallback, undefined, ); diff --git a/apps/cli/src/tools/send/commands/create.command.ts b/apps/cli/src/tools/send/commands/create.command.ts index 09c7937be3a..a9264c50126 100644 --- a/apps/cli/src/tools/send/commands/create.command.ts +++ b/apps/cli/src/tools/send/commands/create.command.ts @@ -3,8 +3,9 @@ import * as fs from "fs"; import * as path from "path"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, switchMap } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; @@ -23,6 +24,7 @@ export class SendCreateCommand { private environmentService: EnvironmentService, private sendApiService: SendApiService, private accountProfileService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} async run(requestJson: any, cmdOptions: Record) { @@ -47,6 +49,8 @@ export class SendCreateCommand { if (req == null) { throw new Error("Null request"); } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.badRequest("Error parsing the encoded request data."); } @@ -78,6 +82,10 @@ export class SendCreateCommand { req.key = null; req.maxAccessCount = maxAccessCount; + const hasPremium$ = this.accountService.activeAccount$.pipe( + switchMap(({ id }) => this.accountProfileService.hasPremiumFromAnySource$(id)), + ); + switch (req.type) { case SendType.File: if (process.env.BW_SERVE === "true") { @@ -86,7 +94,7 @@ export class SendCreateCommand { ); } - if (!(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$))) { + if (!(await firstValueFrom(hasPremium$))) { return Response.error("Premium status is required to use this feature."); } diff --git a/apps/cli/src/tools/send/commands/edit.command.ts b/apps/cli/src/tools/send/commands/edit.command.ts index 2793c450fb6..ed719b58311 100644 --- a/apps/cli/src/tools/send/commands/edit.command.ts +++ b/apps/cli/src/tools/send/commands/edit.command.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; @@ -19,6 +20,7 @@ export class SendEditCommand { private getCommand: SendGetCommand, private sendApiService: SendApiService, private accountProfileService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} async run(requestJson: string, cmdOptions: Record): Promise { @@ -39,6 +41,8 @@ export class SendEditCommand { try { const reqJson = Buffer.from(requestJson, "base64").toString(); req = SendResponse.fromJson(reqJson); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.badRequest("Error parsing the encoded request data."); } @@ -61,8 +65,9 @@ export class SendEditCommand { return Response.badRequest("Cannot change a Send's type"); } + const account = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.accountProfileService.hasPremiumFromAnySource$, + this.accountProfileService.hasPremiumFromAnySource$(account.id), ); if (send.type === SendType.File && !canAccessPremium) { return Response.error("Premium status is required to use this feature."); diff --git a/apps/cli/src/tools/send/commands/receive.command.ts b/apps/cli/src/tools/send/commands/receive.command.ts index d27ba4f88ec..41a6681af55 100644 --- a/apps/cli/src/tools/send/commands/receive.command.ts +++ b/apps/cli/src/tools/send/commands/receive.command.ts @@ -47,6 +47,8 @@ export class SendReceiveCommand extends DownloadCommand { let urlObject: URL; try { urlObject = new URL(url); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.badRequest("Failed to parse the provided Send url"); } diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index b59ae770380..052faa33867 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -258,6 +258,7 @@ export class SendProgram extends BaseProgram { getCmd, this.serviceContainer.sendApiService, this.serviceContainer.billingAccountProfileStateService, + this.serviceContainer.accountService, ); const response = await cmd.run(encodedJson, options); this.processResponse(response); @@ -331,6 +332,7 @@ export class SendProgram extends BaseProgram { this.serviceContainer.environmentService, this.serviceContainer.sendApiService, this.serviceContainer.billingAccountProfileStateService, + this.serviceContainer.accountService, ); return await cmd.run(encodedJson, options); } diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index 1cdf23bcd89..3eb0e68de09 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -113,6 +113,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.organizationUserApiService, this.serviceContainer.apiService, this.serviceContainer.eventCollectionService, + this.serviceContainer.accountService, ); const response = await command.run(object, cmd); @@ -321,6 +322,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.folderApiService, this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.cipherAuthorizationService, + this.serviceContainer.accountService, ); const response = await command.run(object, id, cmd); this.processResponse(response); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index aa01ec3c491..a28d070d19e 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -30,6 +30,8 @@ import { CipherResponse } from "./models/cipher.response"; import { FolderResponse } from "./models/folder.response"; export class CreateCommand { + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + constructor( private cipherService: CipherService, private folderService: FolderService, @@ -64,6 +66,8 @@ export class CreateCommand { try { const reqJson = Buffer.from(requestJson, "base64").toString(); req = JSON.parse(reqJson); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return Response.badRequest("Error parsing the encoded request data."); } @@ -86,9 +90,7 @@ export class CreateCommand { } private async createCipher(req: CipherExport) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId); try { const newCipher = await this.cipherService.createWithServer(cipher); @@ -136,10 +138,13 @@ export class CreateCommand { return Response.notFound(); } - if ( - cipher.organizationId == null && - !(await firstValueFrom(this.accountProfileService.hasPremiumFromAnySource$)) - ) { + const activeUserId = await firstValueFrom(this.activeUserId$); + + const canAccessPremium = await firstValueFrom( + this.accountProfileService.hasPremiumFromAnySource$(activeUserId), + ); + + if (cipher.organizationId == null && !canAccessPremium) { return Response.error("Premium status is required to use this feature."); } @@ -152,9 +157,6 @@ export class CreateCommand { } try { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); const updatedCipher = await this.cipherService.saveAttachmentRawWithServer( cipher, fileName, @@ -171,12 +173,12 @@ export class CreateCommand { } private async createFolder(req: FolderExport) { - const activeAccountId = await firstValueFrom(this.accountService.activeAccount$); - const userKey = await this.keyService.getUserKeyWithLegacySupport(activeAccountId.id); + const activeUserId = await firstValueFrom(this.activeUserId$); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey); try { - await this.folderApiService.save(folder); - const newFolder = await this.folderService.get(folder.id); + await this.folderApiService.save(folder, activeUserId); + const newFolder = await this.folderService.get(folder.id, activeUserId); const decFolder = await newFolder.decrypt(); const res = new FolderResponse(decFolder); return Response.success(res); diff --git a/apps/cli/src/vault/delete.command.ts b/apps/cli/src/vault/delete.command.ts index 7f79c89b17c..a285f8f5b34 100644 --- a/apps/cli/src/vault/delete.command.ts +++ b/apps/cli/src/vault/delete.command.ts @@ -1,6 +1,7 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -19,6 +20,7 @@ export class DeleteCommand { private folderApiService: FolderApiServiceAbstraction, private accountProfileService: BillingAccountProfileStateService, private cipherAuthorizationService: CipherAuthorizationService, + private accountService: AccountService, ) {} async run(object: string, id: string, cmdOptions: Record): Promise { @@ -87,8 +89,9 @@ export class DeleteCommand { return Response.error("Attachment `" + id + "` was not found."); } + const account = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.accountProfileService.hasPremiumFromAnySource$, + this.accountProfileService.hasPremiumFromAnySource$(account.id), ); if (cipher.organizationId == null && !canAccessPremium) { return Response.error("Premium status is required to use this feature."); @@ -103,13 +106,16 @@ export class DeleteCommand { } private async deleteFolder(id: string) { - const folder = await this.folderService.getFromState(id); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const folder = await this.folderService.getFromState(id, activeUserId); if (folder == null) { return Response.notFound(); } try { - await this.folderApiService.delete(id); + await this.folderApiService.delete(id, activeUserId); return Response.success(); } catch (e) { return Response.error(e); diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 3853cd93126..0668ecacdb4 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -26,6 +26,7 @@ "../../libs/tools/export/vault-export/vault-export-core/src" ], "@bitwarden/key-management": ["../../libs/key-management/src"], + "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/node/*": ["../../libs/node/src/*"] }, "plugins": [ diff --git a/apps/desktop/config/config.js b/apps/desktop/config/config.js index 404295dd0db..30a5c16bb2a 100644 --- a/apps/desktop/config/config.js +++ b/apps/desktop/config/config.js @@ -32,6 +32,8 @@ function log(configObj) { function loadConfig(configName) { try { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports return require(`./${configName}.json`); } catch (e) { if (e instanceof Error && e.code === "MODULE_NOT_FOUND") { diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 1fac307262a..b75b68e0b96 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -189,9 +189,9 @@ dependencies = [ [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", @@ -324,28 +324,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-task" version = "4.7.1" @@ -354,9 +332,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0" dependencies = [ "proc-macro2", "quote", @@ -567,9 +545,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.2" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "shlex", ] @@ -616,9 +594,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.22" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -626,9 +604,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.22" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -650,9 +628,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" @@ -703,16 +681,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.0" @@ -740,9 +708,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -759,9 +727,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -821,9 +789,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef" +checksum = "ad7c7515609502d316ab9a24f67dc045132d93bfd3f00713389e90d9898bf30d" dependencies = [ "cc", "cxxbridge-cmd", @@ -835,9 +803,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c" +checksum = "8bfd16fca6fd420aebbd80d643c201ee4692114a0de208b790b9cd02ceae65fb" dependencies = [ "cc", "codespan-reporting", @@ -849,9 +817,9 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6" +checksum = "6c33fd49f5d956a1b7ee5f7a9768d58580c6752838d92e39d0d56439efdedc35" dependencies = [ "clap", "codespan-reporting", @@ -862,15 +830,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa" +checksum = "be0f1077278fac36299cce8446effd19fe93a95eedb10d39265f3bf67b3036c9" [[package]] name = "cxxbridge-macro" -version = "1.0.133" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95" +checksum = "3da7e4d6e74af6b79031d264b2f13c3ea70af1978083741c41ffce9308f1f24f" dependencies = [ "proc-macro2", "quote", @@ -930,12 +898,11 @@ dependencies = [ "anyhow", "arboard", "argon2", - "async-stream", "base64", "bitwarden-russh", "byteorder", "cbc", - "core-foundation 0.10.0", + "core-foundation", "desktop_objc", "dirs", "ed25519", @@ -949,8 +916,6 @@ dependencies = [ "pin-project", "pkcs8", "rand", - "rand_chacha", - "retry", "rsa", "russh-cryptovec", "scopeguard", @@ -996,7 +961,7 @@ version = "0.0.0" dependencies = [ "anyhow", "cc", - "core-foundation 0.9.4", + "core-foundation", "glob", "thiserror", "tokio", @@ -1176,9 +1141,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fiat-crypto" @@ -1200,9 +1165,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "fs-err" @@ -1364,9 +1329,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "goblin" @@ -1518,9 +1483,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.162" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -1643,9 +1608,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] @@ -1677,9 +1642,9 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.3" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" +checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19" [[package]] name = "napi-derive" @@ -1971,9 +1936,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2137,18 +2102,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", @@ -2310,9 +2275,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2375,9 +2340,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags", ] @@ -2422,15 +2387,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "retry" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9166d72162de3575f950507683fac47e30f6f2c3836b71b7fbc61aa517c9c5f4" -dependencies = [ - "rand", -] - [[package]] name = "rsa" version = "0.9.6" @@ -2479,22 +2435,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" @@ -2562,12 +2518,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71" +checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc" dependencies = [ "bitflags", - "core-foundation 0.10.0", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -2575,9 +2531,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -2585,27 +2541,27 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2614,9 +2570,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "itoa", "memchr", @@ -2813,9 +2769,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" dependencies = [ "proc-macro2", "quote", @@ -2824,9 +2780,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" dependencies = [ "core-foundation-sys", "libc", @@ -2838,12 +2794,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3060,9 +3017,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" @@ -3450,13 +3407,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.2.0", - "windows-strings 0.2.0", - "windows-targets 0.52.6", + "windows-result 0.3.0", + "windows-strings 0.3.0", + "windows-targets 0.53.0", ] [[package]] @@ -3477,6 +3434,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34" +dependencies = [ + "windows-targets 0.53.0", +] + [[package]] name = "windows-strings" version = "0.1.0" @@ -3489,11 +3455,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663" +checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.53.0", ] [[package]] @@ -3547,13 +3513,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3566,6 +3548,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3578,6 +3566,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3590,12 +3584,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3608,6 +3614,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3620,6 +3632,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3632,6 +3650,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3645,10 +3669,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.20" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" dependencies = [ "memchr", ] diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 597a082b231..170967f555d 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -6,18 +6,16 @@ version = "0.0.0" publish = false [features] -default = ["sys"] -manual_test = [] - -sys = [ +default = [ "dep:widestring", "dep:windows", "dep:core-foundation", "dep:security-framework", "dep:security-framework-sys", "dep:zbus", - "dep:zbus_polkit", + "dep:zbus_polkit" ] +manual_test = [] [dependencies] aes = "=0.8.4" @@ -26,19 +24,16 @@ arboard = { version = "=3.4.1", default-features = false, features = [ "wayland-data-control", ] } argon2 = { version = "=0.5.3", features = ["zeroize"] } -async-stream = "=0.3.6" base64 = "=0.22.1" byteorder = "=1.5.0" cbc = { version = "=0.1.2", features = ["alloc"] } homedir = "=0.3.4" -libc = "=0.2.162" -pin-project = "=1.1.7" +pin-project = "=1.1.8" dirs = "=5.0.1" futures = "=0.3.31" interprocess = { version = "=2.2.1", features = ["tokio"] } log = "=0.4.22" rand = "=0.8.5" -retry = "=2.0.0" russh-cryptovec = "=0.7.3" scopeguard = "=1.2.0" sha2 = "=0.10.8" @@ -55,7 +50,6 @@ tokio-stream = { version = "=0.1.15", features = ["net"] } tokio-util = { version = "=0.7.12", features = ["codec"] } thiserror = "=1.0.69" typenum = "=1.17.0" -rand_chacha = "=0.3.1" pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] } rsa = "=0.9.6" ed25519 = { version = "=2.2.3", features = ["pkcs8"] } @@ -81,12 +75,13 @@ keytar = "=0.1.6" [target.'cfg(target_os = "macos")'.dependencies] core-foundation = { version = "=0.10.0", optional = true } -security-framework = { version = "=3.0.0", optional = true } -security-framework-sys = { version = "=2.12.0", optional = true } +security-framework = { version = "=3.1.0", optional = true } +security-framework-sys = { version = "=2.13.0", optional = true } desktop_objc = { path = "../objc" } [target.'cfg(target_os = "linux")'.dependencies] oo7 = "=0.3.3" +libc = "=0.2.169" zbus = { version = "=4.4.0", optional = true } zbus_polkit = { version = "=4.0.0", 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 7ad9bcb032e..79be43b1bfc 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -3,12 +3,16 @@ use anyhow::{anyhow, Result}; #[allow(clippy::module_inception)] #[cfg_attr(target_os = "linux", path = "unix.rs")] -#[cfg_attr(target_os = "windows", path = "windows.rs")] #[cfg_attr(target_os = "macos", path = "macos.rs")] +#[cfg_attr(target_os = "windows", path = "windows.rs")] mod biometric; -use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; pub use biometric::Biometric; + +#[cfg(target_os = "windows")] +pub mod windows_focus; + +use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; use sha2::{Digest, Sha256}; use crate::crypto::{self, CipherString}; diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index c91b379c226..70813082faf 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -1,12 +1,15 @@ -use std::{ffi::c_void, str::FromStr}; +use std::{ + ffi::c_void, + str::FromStr, + sync::{atomic::AtomicBool, Arc}, +}; use anyhow::{anyhow, Result}; use base64::{engine::general_purpose::STANDARD as base64_engine, Engine}; use rand::RngCore; -use retry::delay::Fixed; use sha2::{Digest, Sha256}; use windows::{ - core::{factory, h, s, HSTRING}, + core::{factory, h, HSTRING}, Foundation::IAsyncOperation, Security::{ Credentials::{ @@ -14,17 +17,7 @@ use windows::{ }, Cryptography::CryptographicBuffer, }, - Win32::{ - Foundation::HWND, - System::WinRT::IUserConsentVerifierInterop, - UI::{ - Input::KeyboardAndMouse::{ - keybd_event, GetAsyncKeyState, SetFocus, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP, - VK_MENU, - }, - WindowsAndMessaging::{FindWindowA, SetForegroundWindow}, - }, - }, + Win32::{Foundation::HWND, System::WinRT::IUserConsentVerifierInterop}, }; use crate::{ @@ -32,7 +25,10 @@ use crate::{ crypto::CipherString, }; -use super::{decrypt, encrypt}; +use super::{ + decrypt, encrypt, + windows_focus::{focus_security_prompt, set_focus}, +}; /// The Windows OS implementation of the biometric trait. pub struct Biometric {} @@ -103,8 +99,22 @@ impl super::BiometricTrait for Biometric { let challenge_buffer = CryptographicBuffer::CreateFromByteArray(&challenge)?; let async_operation = result.Credential()?.RequestSignAsync(&challenge_buffer)?; - focus_security_prompt()?; - let signature = async_operation.get()?; + focus_security_prompt(); + + let done = Arc::new(AtomicBool::new(false)); + let done_clone = done.clone(); + let _ = std::thread::spawn(move || loop { + if !done_clone.load(std::sync::atomic::Ordering::Relaxed) { + focus_security_prompt(); + std::thread::sleep(std::time::Duration::from_millis(500)); + } else { + break; + } + }); + + let signature = async_operation.get(); + done.store(true, std::sync::atomic::Ordering::Relaxed); + let signature = signature?; if signature.Status()? != KeyCredentialStatus::Success { return Err(anyhow!("Failed to sign data")); @@ -168,57 +178,6 @@ fn random_challenge() -> [u8; 16] { challenge } -/// Searches for a window that looks like a security prompt and set it as focused. -/// -/// Gives up after 1.5 seconds with a delay of 500ms between each try. -fn focus_security_prompt() -> Result<()> { - unsafe fn try_find_and_set_focus( - class_name: windows::core::PCSTR, - ) -> retry::OperationResult<(), ()> { - let hwnd = unsafe { FindWindowA(class_name, None) }; - if let Ok(hwnd) = hwnd { - set_focus(hwnd); - return retry::OperationResult::Ok(()); - } - retry::OperationResult::Retry(()) - } - - let class_name = s!("Credential Dialog Xaml Host"); - retry::retry_with_index(Fixed::from_millis(500), |current_try| { - if current_try > 3 { - return retry::OperationResult::Err(()); - } - - unsafe { try_find_and_set_focus(class_name) } - }) - .map_err(|_| anyhow!("Failed to find security prompt")) -} - -fn set_focus(window: HWND) { - let mut pressed = false; - - unsafe { - // Simulate holding down Alt key to bypass windows limitations - // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getasynckeystate#return-value - // The most significant bit indicates if the key is currently being pressed. This means the - // value will be negative if the key is pressed. - if GetAsyncKeyState(VK_MENU.0 as i32) >= 0 { - pressed = true; - keybd_event(VK_MENU.0 as u8, 0, KEYEVENTF_EXTENDEDKEY, 0); - } - let _ = SetForegroundWindow(window); - let _ = SetFocus(window); - if pressed { - keybd_event( - VK_MENU.0 as u8, - 0, - KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, - 0, - ); - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs b/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs new file mode 100644 index 00000000000..d5e92a67de6 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/biometric/windows_focus.rs @@ -0,0 +1,28 @@ +use windows::{ + core::s, + Win32::{ + Foundation::HWND, + UI::{ + Input::KeyboardAndMouse::SetFocus, + WindowsAndMessaging::{FindWindowA, SetForegroundWindow}, + }, + }, +}; + +/// Searches for a window that looks like a security prompt and set it as focused. +/// Only works when the process has permission to foreground, either by being in foreground +/// Or by being given foreground permission https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow#remarks +pub fn focus_security_prompt() { + let class_name = s!("Credential Dialog Xaml Host"); + let hwnd = unsafe { FindWindowA(class_name, None) }; + if let Ok(hwnd) = hwnd { + set_focus(hwnd); + } +} + +pub(crate) fn set_focus(window: HWND) { + unsafe { + let _ = SetForegroundWindow(window); + let _ = SetFocus(window); + } +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index b63c773209f..4a6686cc1f5 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -1,16 +1,10 @@ pub mod autofill; -#[cfg(feature = "sys")] pub mod biometric; -#[cfg(feature = "sys")] pub mod clipboard; pub mod crypto; pub mod error; pub mod ipc; -#[cfg(feature = "sys")] pub mod password; -#[cfg(feature = "sys")] pub mod powermonitor; -#[cfg(feature = "sys")] pub mod process_isolation; -#[cfg(feature = "sys")] pub mod ssh_agent; diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs index 32300b9f81f..8b297fc33b7 100644 --- a/apps/desktop/desktop_native/core/src/password/windows.rs +++ b/apps/desktop/desktop_native/core/src/password/windows.rs @@ -13,7 +13,7 @@ use windows::{ const CRED_FLAGS_NONE: u32 = 0; -pub async fn get_password<'a>(service: &str, account: &str) -> Result { +pub async fn get_password(service: &str, account: &str) -> Result { let target_name = U16CString::from_str(target_name(service, account))?; let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs b/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs deleted file mode 100644 index f532d0ab280..00000000000 --- a/apps/desktop/desktop_native/core/src/ssh_agent/generator.rs +++ /dev/null @@ -1,45 +0,0 @@ -use rand::SeedableRng; -use rand_chacha::ChaCha8Rng; -use ssh_key::{Algorithm, HashAlg, LineEnding}; - -use super::importer::SshKey; - -pub async fn generate_keypair(key_algorithm: String) -> Result { - // sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom - // if it cannot be securely sourced, this will panic instead of leading to a weak key - let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy(); - - let key = match key_algorithm.as_str() { - "ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519), - "rsa2048" | "rsa3072" | "rsa4096" => { - let bits = match key_algorithm.as_str() { - "rsa2048" => 2048, - "rsa3072" => 3072, - "rsa4096" => 4096, - _ => return Err(anyhow::anyhow!("Unsupported RSA key size")), - }; - let rsa_keypair = ssh_key::private::RsaKeypair::random(&mut rng, bits) - .map_err(|e| anyhow::anyhow!(e.to_string()))?; - - let private_key = ssh_key::PrivateKey::new( - ssh_key::private::KeypairData::from(rsa_keypair), - "".to_string(), - ) - .map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(private_key) - } - _ => { - return Err(anyhow::anyhow!("Unsupported key algorithm")); - } - } - .map_err(|e| anyhow::anyhow!(e.to_string()))?; - - let private_key_openssh = key - .to_openssh(LineEnding::LF) - .map_err(|e| anyhow::anyhow!(e.to_string()))?; - Ok(SshKey { - private_key: private_key_openssh.to_string(), - public_key: key.public_key().to_string(), - key_fingerprint: key.fingerprint(HashAlg::Sha256).to_string(), - }) -} 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 eced6e89545..4e304ccea78 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs @@ -16,7 +16,6 @@ mod platform_ssh_agent; #[cfg(any(target_os = "linux", target_os = "macos"))] mod peercred_unix_listener_stream; -pub mod generator; pub mod importer; pub mod peerinfo; #[derive(Clone)] diff --git a/apps/desktop/desktop_native/macos_provider/Cargo.toml b/apps/desktop/desktop_native/macos_provider/Cargo.toml index 28cc6372c62..ff7408d6d44 100644 --- a/apps/desktop/desktop_native/macos_provider/Cargo.toml +++ b/apps/desktop/desktop_native/macos_provider/Cargo.toml @@ -21,10 +21,10 @@ serde = { version = "1.0.205", features = ["derive"] } serde_json = "1.0.122" tokio = { version = "1.39.2", features = ["sync"] } tokio-util = "0.7.11" -uniffi = { version = "0.28.0", features = ["cli"] } +uniffi = { version = "0.28.3", features = ["cli"] } [target.'cfg(target_os = "macos")'.dependencies] oslog = "0.2.0" [build-dependencies] -uniffi = { version = "0.28.0", features = ["build"] } +uniffi = { version = "0.28.3", features = ["build"] } diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 6a656cdc574..974948e254b 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -27,7 +27,7 @@ tokio-util = "=0.7.12" tokio-stream = "=0.1.15" [target.'cfg(windows)'.dependencies] -windows-registry = "=0.3.0" +windows-registry = "=0.4.0" [build-dependencies] -napi-build = "=2.1.3" +napi-build = "=2.1.4" diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 1f37563e4fe..997e951c89e 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -74,7 +74,6 @@ export declare namespace sshagent { export function lock(agentState: SshAgentState): void export function importKey(encodedKey: string, password: string): SshKeyImportResult export function clearKeys(agentState: SshAgentState): void - export function generateKeypair(keyAlgorithm: string): Promise export class SshAgentState { } } export declare namespace processisolations { diff --git a/apps/desktop/desktop_native/napi/index.js b/apps/desktop/desktop_native/napi/index.js index a0cfee8e1a0..acfd0dffb89 100644 --- a/apps/desktop/desktop_native/napi/index.js +++ b/apps/desktop/desktop_native/napi/index.js @@ -1,209 +1,132 @@ -const { existsSync, readFileSync } = require('fs') -const { join } = require('path') +const { existsSync } = require("fs"); +const { join } = require("path"); -const { platform, arch } = process +const { platform, arch } = process; -let nativeBinding = null -let localFileExisted = false -let loadError = null +let nativeBinding = null; +let localFileExisted = false; +let loadError = null; -function isMusl() { - // For Node 10 - if (!process.report || typeof process.report.getReport !== 'function') { - try { - return readFileSync('/usr/bin/ldd', 'utf8').includes('musl') - } catch (e) { - return true +function loadFirstAvailable(localFiles, nodeModule) { + for (const localFile of localFiles) { + if (existsSync(join(__dirname, localFile))) { + return require(`./${localFile}`); } - } else { - const { glibcVersionRuntime } = process.report.getReport().header - return !glibcVersionRuntime } + + require(nodeModule); } switch (platform) { - case 'android': + case "android": switch (arch) { - case 'arm64': - localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.android-arm64.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-android-arm64') - } - } catch (e) { - loadError = e - } - break - case 'arm': - localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm-eabi.node')) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.android-arm-eabi.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-android-arm-eabi') - } - } catch (e) { - loadError = e - } - break + case "arm64": + nativeBinding = loadFirstAvailable( + ["desktop_napi.android-arm64.node"], + "@bitwarden/desktop-napi-android-arm64", + ); + break; + case "arm": + nativeBinding = loadFirstAvailable( + ["desktop_napi.android-arm.node"], + "@bitwarden/desktop-napi-android-arm", + ); + break; default: - throw new Error(`Unsupported architecture on Android ${arch}`) + throw new Error(`Unsupported architecture on Android ${arch}`); } - break - case 'win32': + break; + case "win32": switch (arch) { - case 'x64': - localFileExisted = existsSync( - join(__dirname, 'desktop_napi.win32-x64-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.win32-x64-msvc.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-win32-x64-msvc') - } - } catch (e) { - loadError = e - } - break - case 'ia32': - localFileExisted = existsSync( - join(__dirname, 'desktop_napi.win32-ia32-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.win32-ia32-msvc.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-win32-ia32-msvc') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'desktop_napi.win32-arm64-msvc.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.win32-arm64-msvc.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-win32-arm64-msvc') - } - } catch (e) { - loadError = e - } - break + case "x64": + nativeBinding = loadFirstAvailable( + ["desktop_napi.win32-x64-msvc.node"], + "@bitwarden/desktop-napi-win32-x64-msvc", + ); + break; + case "ia32": + nativeBinding = loadFirstAvailable( + ["desktop_napi.win32-ia32-msvc.node"], + "@bitwarden/desktop-napi-win32-ia32-msvc", + ); + break; + case "arm64": + nativeBinding = loadFirstAvailable( + ["desktop_napi.win32-arm64-msvc.node"], + "@bitwarden/desktop-napi-win32-arm64-msvc", + ); + break; default: - throw new Error(`Unsupported architecture on Windows: ${arch}`) + throw new Error(`Unsupported architecture on Windows: ${arch}`); } - break - case 'darwin': + break; + case "darwin": switch (arch) { - case 'x64': - localFileExisted = existsSync(join(__dirname, 'desktop_napi.darwin-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.darwin-x64.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-darwin-x64') - } - } catch (e) { - loadError = e - } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'desktop_napi.darwin-arm64.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.darwin-arm64.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-darwin-arm64') - } - } catch (e) { - loadError = e - } - break + case "x64": + nativeBinding = loadFirstAvailable( + ["desktop_napi.darwin-x64.node"], + "@bitwarden/desktop-napi-darwin-x64", + ); + break; + case "arm64": + nativeBinding = loadFirstAvailable( + ["desktop_napi.darwin-arm64.node"], + "@bitwarden/desktop-napi-darwin-arm64", + ); + break; default: - throw new Error(`Unsupported architecture on macOS: ${arch}`) + throw new Error(`Unsupported architecture on macOS: ${arch}`); } - break - case 'freebsd': - if (arch !== 'x64') { - throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) - } - localFileExisted = existsSync(join(__dirname, 'desktop_napi.freebsd-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.freebsd-x64.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-freebsd-x64') - } - } catch (e) { - loadError = e - } - break - case 'linux': + break; + case "freebsd": + nativeBinding = loadFirstAvailable( + ["desktop_napi.freebsd-x64.node"], + "@bitwarden/desktop-napi-freebsd-x64", + ); + break; + case "linux": switch (arch) { - case 'x64': - localFileExisted = existsSync( - join(__dirname, 'desktop_napi.linux-x64-musl.node') - ) + case "x64": + nativeBinding = loadFirstAvailable( + ["desktop_napi.linux-x64-musl.node", "desktop_napi.linux-x64-gnu.node"], + "@bitwarden/desktop-napi-linux-x64-musl", + ); + break; + case "arm64": + nativeBinding = loadFirstAvailable( + ["desktop_napi.linux-arm64-musl.node", "desktop_napi.linux-arm64-gnu.node"], + "@bitwarden/desktop-napi-linux-arm64-musl", + ); + break; + case "arm": + nativeBinding = loadFirstAvailable( + ["desktop_napi.linux-arm-musl.node", "desktop_napi.linux-arm-gnu.node"], + "@bitwarden/desktop-napi-linux-arm-musl", + ); + localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node")); try { if (localFileExisted) { - nativeBinding = require('./desktop_napi.linux-x64-musl.node') + nativeBinding = require("./desktop_napi.linux-arm-gnueabihf.node"); } else { - nativeBinding = require('@bitwarden/desktop-napi-linux-x64-musl') + nativeBinding = require("@bitwarden/desktop-napi-linux-arm-gnueabihf"); } } catch (e) { - loadError = e + loadError = e; } - break - case 'arm64': - localFileExisted = existsSync( - join(__dirname, 'desktop_napi.linux-arm64-musl.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.linux-arm64-musl.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-linux-arm64-musl') - } - } catch (e) { - loadError = e - } - break - case 'arm': - localFileExisted = existsSync( - join(__dirname, 'desktop_napi.linux-arm-gnueabihf.node') - ) - try { - if (localFileExisted) { - nativeBinding = require('./desktop_napi.linux-arm-gnueabihf.node') - } else { - nativeBinding = require('@bitwarden/desktop-napi-linux-arm-gnueabihf') - } - } catch (e) { - loadError = e - } - break + break; default: - throw new Error(`Unsupported architecture on Linux: ${arch}`) + throw new Error(`Unsupported architecture on Linux: ${arch}`); } - break + break; default: - throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); } if (!nativeBinding) { if (loadError) { - throw loadError + throw loadError; } - throw new Error(`Failed to load native binding`) + throw new Error(`Failed to load native binding`); } -module.exports = nativeBinding +module.exports = nativeBinding; diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 3ceef666d80..35566e16813 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -362,14 +362,6 @@ pub mod sshagent { .clear_keys() .map_err(|e| napi::Error::from_reason(e.to_string())) } - - #[napi] - pub async fn generate_keypair(key_algorithm: String) -> napi::Result { - desktop_core::ssh_agent::generator::generate_keypair(key_algorithm) - .await - .map_err(|e| napi::Error::from_reason(e.to_string())) - .map(|k| k.into()) - } } #[napi] diff --git a/apps/desktop/desktop_native/objc/Cargo.toml b/apps/desktop/desktop_native/objc/Cargo.toml index cfcc92bef7a..eae303b9f08 100644 --- a/apps/desktop/desktop_native/objc/Cargo.toml +++ b/apps/desktop/desktop_native/objc/Cargo.toml @@ -14,8 +14,8 @@ thiserror = "=1.0.69" tokio = "1.39.1" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "=0.9.4" +core-foundation = "=0.10.0" [build-dependencies] -cc = "1.0.104" -glob = "0.3.1" +cc = "1.2.4" +glob = "0.3.2" diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m index 8b73635a7ca..fc13c04591a 100644 --- a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -30,21 +30,24 @@ void runSync(void* context, NSDictionary *params) { [mappedCredentials addObject:credential]; } - if ([type isEqualToString:@"fido2"]) { - NSString *cipherId = credential[@"cipherId"]; - NSString *rpId = credential[@"rpId"]; - NSString *userName = credential[@"userName"]; - NSData *credentialId = decodeBase64URL(credential[@"credentialId"]); - NSData *userHandle = decodeBase64URL(credential[@"userHandle"]); + if (@available(macos 14, *)) { + if ([type isEqualToString:@"fido2"]) { + NSString *cipherId = credential[@"cipherId"]; + NSString *rpId = credential[@"rpId"]; + NSString *userName = credential[@"userName"]; + NSData *credentialId = decodeBase64URL(credential[@"credentialId"]); + NSData *userHandle = decodeBase64URL(credential[@"userHandle"]); - ASPasskeyCredentialIdentity *credential = [[ASPasskeyCredentialIdentity alloc] - initWithRelyingPartyIdentifier:rpId - userName:userName - credentialID:credentialId - userHandle:userHandle - recordIdentifier:cipherId]; + Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity"); + id credential = [[passkeyCredentialIdentityClass alloc] + initWithRelyingPartyIdentifier:rpId + userName:userName + credentialID:credentialId + userHandle:userHandle + recordIdentifier:cipherId]; - [mappedCredentials addObject:credential]; + [mappedCredentials addObject:credential]; + } } } diff --git a/apps/desktop/desktop_native/proxy/Cargo.toml b/apps/desktop/desktop_native/proxy/Cargo.toml index 06e587b442d..3618a11a921 100644 --- a/apps/desktop/desktop_native/proxy/Cargo.toml +++ b/apps/desktop/desktop_native/proxy/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] anyhow = "=1.0.94" -desktop_core = { path = "../core", default-features = false } +desktop_core = { path = "../core" } futures = "=0.3.31" log = "=0.4.22" simplelog = "=0.12.2" diff --git a/apps/desktop/desktop_native/proxy/src/main.rs b/apps/desktop/desktop_native/proxy/src/main.rs index bfd84b206d7..ba29e00cf13 100644 --- a/apps/desktop/desktop_native/proxy/src/main.rs +++ b/apps/desktop/desktop_native/proxy/src/main.rs @@ -5,6 +5,9 @@ use futures::{FutureExt, SinkExt, StreamExt}; use log::*; use tokio_util::codec::LengthDelimitedCodec; +#[cfg(target_os = "windows")] +mod windows; + #[cfg(target_os = "macos")] embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist"); @@ -49,6 +52,9 @@ fn init_logging(log_path: &Path, console_level: LevelFilter, file_level: LevelFi /// #[tokio::main(flavor = "current_thread")] async fn main() { + #[cfg(target_os = "windows")] + let should_foreground = windows::allow_foreground(); + let sock_path = desktop_core::ipc::path("bitwarden"); let log_path = { @@ -142,6 +148,9 @@ async fn main() { // Listen to stdin and send messages to ipc processor. msg = stdin.next() => { + #[cfg(target_os = "windows")] + should_foreground.store(true, std::sync::atomic::Ordering::Relaxed); + match msg { Some(Ok(msg)) => { let m = String::from_utf8(msg.to_vec()).unwrap(); diff --git a/apps/desktop/desktop_native/proxy/src/windows.rs b/apps/desktop/desktop_native/proxy/src/windows.rs new file mode 100644 index 00000000000..cb0656fc7f8 --- /dev/null +++ b/apps/desktop/desktop_native/proxy/src/windows.rs @@ -0,0 +1,23 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +pub fn allow_foreground() -> Arc { + let should_foreground = Arc::new(AtomicBool::new(false)); + let should_foreground_clone = should_foreground.clone(); + let _ = std::thread::spawn(move || loop { + if !should_foreground_clone.load(Ordering::Relaxed) { + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + should_foreground_clone.store(false, Ordering::Relaxed); + + for _ in 0..60 { + desktop_core::biometric::windows_focus::focus_security_prompt(); + std::thread::sleep(std::time::Duration::from_millis(1000)); + } + }); + + should_foreground +} diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 898ad086b29..4302f302473 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -20,7 +20,7 @@ "**/node_modules/@bitwarden/desktop-napi/index.js", "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node" ], - "electronVersion": "33.2.1", + "electronVersion": "33.3.1", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", @@ -133,7 +133,7 @@ "entitlements": "resources/entitlements.mas.plist", "entitlementsInherit": "resources/entitlements.mas.inherit.plist", "entitlementsLoginHelper": "resources/entitlements.mas.loginhelper.plist", - "hardenedRuntime": false, + "hardenedRuntime": true, "extendInfo": { "LSMinimumSystemVersion": "12", "ElectronTeamID": "LTZ2PFU5D6" diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 469f8bd1da1..f727c903a7f 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -14,11 +14,11 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "11.0.3", + "uuid": "11.0.5", "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.1", + "@types/node": "22.10.5", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } @@ -106,9 +106,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -421,9 +421,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", - "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index e32b9e8e987..b7da729d7d1 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -19,11 +19,11 @@ "module-alias": "2.2.3", "node-ipc": "9.2.1", "ts-node": "10.9.2", - "uuid": "11.0.3", + "uuid": "11.0.5", "yargs": "17.7.2" }, "devDependencies": { - "@types/node": "22.10.1", + "@types/node": "22.10.5", "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts index 2f8e61c68a4..8d2d734677a 100644 --- a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts @@ -7,6 +7,7 @@ import { hideBin } from "yargs/helpers"; import { NativeMessagingVersion } from "@bitwarden/common/enums"; +// eslint-disable-next-line no-restricted-imports import { CredentialCreatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-create-payload"; import { LogUtils } from "../log-utils"; import NativeMessageService from "../native-message.service"; diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts index 4f17bb5658a..93598bf9eef 100644 --- a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts @@ -7,6 +7,7 @@ import { hideBin } from "yargs/helpers"; import { NativeMessagingVersion } from "@bitwarden/common/enums"; +// eslint-disable-next-line no-restricted-imports import { CredentialUpdatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-update-payload"; import { LogUtils } from "../log-utils"; import NativeMessageService from "../native-message.service"; diff --git a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts index ee17e672a79..8513363956e 100644 --- a/apps/desktop/native-messaging-test-runner/src/ipc.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/ipc.service.ts @@ -4,7 +4,9 @@ import { homedir } from "os"; import * as NodeIPC from "node-ipc"; +// eslint-disable-next-line no-restricted-imports import { MessageCommon } from "../../src/models/native-messaging/message-common"; +// eslint-disable-next-line no-restricted-imports import { UnencryptedMessageResponse } from "../../src/models/native-messaging/unencrypted-message-response"; import Deferred from "./deferred"; diff --git a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts index cd84504c630..94fdde026b2 100644 --- a/apps/desktop/native-messaging-test-runner/src/native-message.service.ts +++ b/apps/desktop/native-messaging-test-runner/src/native-message.service.ts @@ -9,13 +9,21 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; +// eslint-disable-next-line no-restricted-imports import { DecryptedCommandData } from "../../src/models/native-messaging/decrypted-command-data"; +// eslint-disable-next-line no-restricted-imports import { EncryptedMessage } from "../../src/models/native-messaging/encrypted-message"; +// eslint-disable-next-line no-restricted-imports import { CredentialCreatePayload } from "../../src/models/native-messaging/encrypted-message-payloads/credential-create-payload"; +// eslint-disable-next-line no-restricted-imports import { CredentialUpdatePayload } from "../../src/models/native-messaging/encrypted-message-payloads/credential-update-payload"; +// eslint-disable-next-line no-restricted-imports import { EncryptedMessageResponse } from "../../src/models/native-messaging/encrypted-message-response"; +// eslint-disable-next-line no-restricted-imports import { MessageCommon } from "../../src/models/native-messaging/message-common"; +// eslint-disable-next-line no-restricted-imports import { UnencryptedMessage } from "../../src/models/native-messaging/unencrypted-message"; +// eslint-disable-next-line no-restricted-imports import { UnencryptedMessageResponse } from "../../src/models/native-messaging/unencrypted-message-response"; import IPCService, { IPCOptions } from "./ipc.service"; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7c6d1f37692..249145eb3ea 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": "2024.12.2", + "version": "2025.1.4", "keywords": [ "bitwarden", "password", @@ -35,7 +35,7 @@ "clean:dist": "rimraf ./dist", "pack:dir": "npm run clean:dist && electron-builder --dir -p never", "pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", - "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && mksquashfs ./dist/tmp-snap/ $SNAP_FILE -noappend -comp lzo -no-fragments && rm -rf ./dist/tmp-snap/", + "pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snapcraft pack ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/", "pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never", "pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never", "pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never", diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js index c4513687e89..c39e7ea0355 100644 --- a/apps/desktop/postcss.config.js +++ b/apps/desktop/postcss.config.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef */ +/* eslint-disable @typescript-eslint/no-require-imports, no-undef */ module.exports = { plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")], }; diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml index 234d37905cc..fea28052f8d 100644 --- a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml +++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml @@ -8,7 +8,6 @@ command: bitwarden.sh finish-args: - --share=ipc - --share=network - - --socket=wayland - --socket=x11 - --device=dri - --env=XDG_CURRENT_DESKTOP=Unity @@ -33,11 +32,14 @@ modules: - install bitwarden.sh /app/bin/bitwarden.sh sources: - type: dir + only-arches: [x86_64] path: ../dist/linux-unpacked + - type: dir + only-arches: [aarch64] + path: ../dist/linux-arm64-unpacked - type: script dest-filename: bitwarden.sh commands: - ulimit -c 0 - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" - - exec zypak-wrapper /app/bin/bitwarden-app --ozone-platform-hint=auto - --enable-features=WaylandWindowDecorations "$@" + - exec zypak-wrapper /app/bin/bitwarden-app "$@" diff --git a/apps/desktop/resources/entitlements.desktop_proxy.inherit.plist b/apps/desktop/resources/entitlements.desktop_proxy.inherit.plist index 794eada1cad..fca5f02d52d 100644 --- a/apps/desktop/resources/entitlements.desktop_proxy.inherit.plist +++ b/apps/desktop/resources/entitlements.desktop_proxy.inherit.plist @@ -6,5 +6,7 @@ com.apple.security.inherit + com.apple.security.cs.allow-jit + diff --git a/apps/desktop/resources/entitlements.desktop_proxy.plist b/apps/desktop/resources/entitlements.desktop_proxy.plist index d5c7b8a2cc8..1a39a482389 100644 --- a/apps/desktop/resources/entitlements.desktop_proxy.plist +++ b/apps/desktop/resources/entitlements.desktop_proxy.plist @@ -8,5 +8,7 @@ LTZ2PFU5D6.com.bitwarden.desktop + com.apple.security.cs.allow-jit + diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist index 34c561bd03f..e273bcc7eca 100644 --- a/apps/desktop/resources/entitlements.mac.plist +++ b/apps/desktop/resources/entitlements.mac.plist @@ -4,10 +4,6 @@ com.apple.security.cs.allow-jit - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-library-validation -
    @@ -638,33 +648,35 @@
    + +

    {{ "deviceListDescriptionTemp" | i18n }}

    + +
    + +
    + + + + + {{ col.title }} + + + + + +
    + +
    +
    + + + {{ row.displayName }} + + + + {{ "needsApproval" | i18n }} + + + + {{ row.displayName }} + + {{ "trusted" | i18n }} + + +
    + + + {{ + "currentSession" | i18n + }} + {{ + "requestPending" | i18n + }} + + {{ row.firstLogin | date: "medium" }} + +
    +
    + diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts new file mode 100644 index 00000000000..e22122ad9ae --- /dev/null +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -0,0 +1,243 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { combineLatest, firstValueFrom } from "rxjs"; + +import { LoginApprovalComponent } from "@bitwarden/auth/angular"; +import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; +import { + DevicePendingAuthRequest, + DeviceResponse, +} from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; +import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; +import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + DialogService, + ToastService, + TableDataSource, + TableModule, + PopoverModule, +} from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +interface DeviceTableData { + id: string; + type: DeviceType; + displayName: string; + loginStatus: string; + firstLogin: Date; + trusted: boolean; + devicePendingAuthRequest: DevicePendingAuthRequest | null; + hasPendingAuthRequest: boolean; +} + +/** + * Provides a table of devices and allows the user to log out, approve or remove a device + */ +@Component({ + selector: "app-device-management", + templateUrl: "./device-management.component.html", + standalone: true, + imports: [CommonModule, SharedModule, TableModule, PopoverModule], +}) +export class DeviceManagementComponent { + protected readonly tableId = "device-management-table"; + protected dataSource = new TableDataSource(); + protected currentDevice: DeviceView | undefined; + protected loading = true; + protected asyncActionLoading = false; + + constructor( + private i18nService: I18nService, + private devicesService: DevicesServiceAbstraction, + private dialogService: DialogService, + private toastService: ToastService, + private validationService: ValidationService, + ) { + combineLatest([this.devicesService.getCurrentDevice$(), this.devicesService.getDevices$()]) + .pipe(takeUntilDestroyed()) + .subscribe({ + next: ([currentDevice, devices]: [DeviceResponse, Array]) => { + this.currentDevice = new DeviceView(currentDevice); + + this.dataSource.data = devices.map((device: DeviceView): DeviceTableData => { + return { + id: device.id, + type: device.type, + displayName: this.getHumanReadableDeviceType(device.type), + loginStatus: this.getLoginStatus(device), + firstLogin: new Date(device.creationDate), + trusted: device.response.isTrusted, + devicePendingAuthRequest: device.response.devicePendingAuthRequest, + hasPendingAuthRequest: this.hasPendingAuthRequest(device.response), + }; + }); + + this.loading = false; + }, + error: () => { + this.loading = false; + }, + }); + } + + /** + * Column configuration for the table + */ + protected readonly columnConfig = [ + { + name: "displayName", + title: this.i18nService.t("device"), + headerClass: "tw-w-1/3", + sortable: true, + }, + { + name: "loginStatus", + title: this.i18nService.t("loginStatus"), + headerClass: "tw-w-1/3", + sortable: true, + }, + { + name: "firstLogin", + title: this.i18nService.t("firstLogin"), + headerClass: "tw-w-1/3", + sortable: true, + }, + ]; + + /** + * Get the icon for a device type + * @param type - The device type + * @returns The icon for the device type + */ + getDeviceIcon(type: DeviceType): string { + const defaultIcon = "bwi bwi-desktop"; + const categoryIconMap: Record = { + webVault: "bwi bwi-browser", + desktop: "bwi bwi-desktop", + mobile: "bwi bwi-mobile", + cli: "bwi bwi-cli", + extension: "bwi bwi-puzzle", + sdk: "bwi bwi-desktop", + }; + + const metadata = DeviceTypeMetadata[type]; + return metadata ? (categoryIconMap[metadata.category] ?? defaultIcon) : defaultIcon; + } + + /** + * Get the login status of a device + * It will return the current session if the device is the current device + * It will return the date of the pending auth request when available + * @param device - The device + * @returns The login status + */ + private getLoginStatus(device: DeviceView): string { + if (this.isCurrentDevice(device)) { + return this.i18nService.t("currentSession"); + } + + if (device.response.devicePendingAuthRequest?.creationDate) { + return this.i18nService.t("requestPending"); + } + + return ""; + } + + /** + * Get a human readable device type from the DeviceType enum + * @param type - The device type + * @returns The human readable device type + */ + private getHumanReadableDeviceType(type: DeviceType): string { + const metadata = DeviceTypeMetadata[type]; + if (!metadata) { + return this.i18nService.t("unknownDevice"); + } + + // If the platform is "Unknown" translate it since it is not a proper noun + const platform = + metadata.platform === "Unknown" ? this.i18nService.t("unknown") : metadata.platform; + const category = this.i18nService.t(metadata.category); + return platform ? `${category} - ${platform}` : category; + } + + /** + * Check if a device is the current device + * @param device - The device or device table data + * @returns True if the device is the current device, false otherwise + */ + protected isCurrentDevice(device: DeviceView | DeviceTableData): boolean { + return "response" in device + ? device.id === this.currentDevice?.id + : device.id === this.currentDevice?.id; + } + + /** + * Check if a device has a pending auth request + * @param device - The device response + * @returns True if the device has a pending auth request, false otherwise + */ + private hasPendingAuthRequest(device: DeviceResponse): boolean { + return ( + device.devicePendingAuthRequest !== undefined && device.devicePendingAuthRequest !== null + ); + } + + /** + * Open a dialog to approve or deny a pending auth request for a device + */ + async managePendingAuthRequest(device: DeviceTableData) { + if (device.devicePendingAuthRequest === undefined || device.devicePendingAuthRequest === null) { + return; + } + + const dialogRef = LoginApprovalComponent.open(this.dialogService, { + notificationId: device.devicePendingAuthRequest.id, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result !== undefined && typeof result === "boolean") { + // auth request approved or denied so reset + device.devicePendingAuthRequest = null; + device.hasPendingAuthRequest = false; + } + } + + /** + * Remove a device + * @param device - The device + */ + protected async removeDevice(device: DeviceTableData) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "removeDevice" }, + content: { key: "removeDeviceConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + this.asyncActionLoading = true; + await firstValueFrom(this.devicesService.deactivateDevice$(device.id)); + this.asyncActionLoading = false; + + // Remove the device from the data source + this.dataSource.data = this.dataSource.data.filter((d) => d.id !== device.id); + + this.toastService.showToast({ + title: "", + message: this.i18nService.t("deviceRemoved"), + variant: "success", + }); + } catch (error) { + this.validationService.showError(error); + } + } +} diff --git a/apps/web/src/app/auth/settings/security/security-routing.module.ts b/apps/web/src/app/auth/settings/security/security-routing.module.ts index 8af0499d05a..6ed21605184 100644 --- a/apps/web/src/app/auth/settings/security/security-routing.module.ts +++ b/apps/web/src/app/auth/settings/security/security-routing.module.ts @@ -4,6 +4,7 @@ import { RouterModule, Routes } from "@angular/router"; import { ChangePasswordComponent } from "../change-password.component"; import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component"; +import { DeviceManagementComponent } from "./device-management.component"; import { SecurityKeysComponent } from "./security-keys.component"; import { SecurityComponent } from "./security.component"; @@ -29,6 +30,11 @@ const routes: Routes = [ component: SecurityKeysComponent, data: { titleId: "keys" }, }, + { + path: "device-management", + component: DeviceManagementComponent, + data: { titleId: "devices" }, + }, ], }, ]; diff --git a/apps/web/src/app/auth/settings/security/security.component.html b/apps/web/src/app/auth/settings/security/security.component.html index 25459faeacc..6bd7c1daf36 100644 --- a/apps/web/src/app/auth/settings/security/security.component.html +++ b/apps/web/src/app/auth/settings/security/security.component.html @@ -4,6 +4,7 @@ {{ "masterPassword" | i18n }}
    {{ "twoStepLogin" | i18n }} + {{ "devices" | i18n }} {{ "keys" | i18n }} diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 1df8145a917..d643b565df2 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @Component({ selector: "app-security", @@ -9,7 +10,10 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use export class SecurityComponent implements OnInit { showChangePassword = true; - constructor(private userVerificationService: UserVerificationService) {} + constructor( + private userVerificationService: UserVerificationService, + private configService: ConfigService, + ) {} async ngOnInit() { this.showChangePassword = await this.userVerificationService.hasMasterPassword(); diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 14cf63d3f4e..3b20718873d 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -10,6 +10,7 @@ import { Subject, Subscription, takeUntil, + switchMap, } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -18,6 +19,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response"; import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; @@ -69,8 +71,13 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { protected messagingService: MessagingService, protected policyService: PolicyService, billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) { - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); } async ngOnInit() { diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html index f927a7ca613..2f983944b70 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.html @@ -58,7 +58,7 @@ *ngIf="deprecateStripeSourcesAPI" [showAccountCredit]="false" > - +
    - diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts index 4ed35461c72..270ba54f70d 100644 --- a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Location } from "@angular/common"; -import { Component, OnDestroy, ViewChild } from "@angular/core"; +import { Component, OnDestroy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute, Router } from "@angular/router"; import { from, lastValueFrom, switchMap } from "rxjs"; @@ -11,7 +11,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; @@ -22,7 +21,6 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { FreeTrial } from "../../../core/types/free-trial"; import { TrialFlowService } from "../../services/trial-flow.service"; -import { TaxInfoComponent } from "../../shared"; import { AddCreditDialogResult, openAddCreditDialog, @@ -36,8 +34,6 @@ import { templateUrl: "./organization-payment-method.component.html", }) export class OrganizationPaymentMethodComponent implements OnDestroy { - @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; - organizationId: string; isUnpaid = false; accountCredit: number; @@ -155,6 +151,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, + productTier: this.organization?.productTierType, }, }); @@ -170,6 +167,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { data: { initialPaymentMethod: this.paymentSource?.type, organizationId: this.organizationId, + productTier: this.organization?.productTierType, }, }); const result = await lastValueFrom(dialogRef.closed); @@ -183,32 +181,6 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { } }; - protected updateTaxInformation = async (): Promise => { - this.taxInfoComponent.taxFormGroup.updateValueAndValidity(); - this.taxInfoComponent.taxFormGroup.markAllAsTouched(); - - if (this.taxInfoComponent.taxFormGroup.invalid) { - return; - } - - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = this.taxInfoComponent.country; - request.postalCode = this.taxInfoComponent.postalCode; - request.taxId = this.taxInfoComponent.taxId; - request.line1 = this.taxInfoComponent.line1; - request.line2 = this.taxInfoComponent.line2; - request.city = this.taxInfoComponent.city; - request.state = this.taxInfoComponent.state; - - await this.billingApiService.updateOrganizationTaxInformation(this.organizationId, request); - - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("taxInfoUpdated"), - }); - }; - protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request); this.toastService.showToast({ diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index 4a4f309c68b..fc7a188f967 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -5,6 +5,8 @@ import { FormBuilder, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data"; import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -104,6 +106,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private toastService: ToastService, + private internalOrganizationService: InternalOrganizationServiceAbstraction, ) {} ngOnInit() { @@ -157,11 +160,20 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest ? this.formGroup.value.maxAutoscaleServiceAccounts : null; - await this.organizationApiService.updateSecretsManagerSubscription( + const response = await this.organizationApiService.updateSecretsManagerSubscription( this.organizationId, request, ); + const organization = await this.internalOrganizationService.get(this.organizationId); + + const organizationData = new OrganizationData(response, { + isMember: organization.isMember, + isProviderUser: organization.isProviderUser, + }); + + await this.internalOrganizationService.upsert(organizationData); + this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/billing/services/reseller-warning.service.ts b/apps/web/src/app/billing/services/reseller-warning.service.ts new file mode 100644 index 00000000000..2c59ebafe05 --- /dev/null +++ b/apps/web/src/app/billing/services/reseller-warning.service.ts @@ -0,0 +1,142 @@ +import { Injectable } from "@angular/core"; + +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +export interface ResellerWarning { + type: "info" | "warning"; + message: string; +} + +@Injectable({ providedIn: "root" }) +export class ResellerWarningService { + private readonly RENEWAL_WARNING_DAYS = 14; + private readonly GRACE_PERIOD_DAYS = 30; + + constructor(private i18nService: I18nService) {} + + getWarning( + organization: Organization, + organizationBillingMetadata: OrganizationBillingMetadataResponse, + ): ResellerWarning | null { + if (!organization.hasReseller) { + return null; // If no reseller, return null immediately + } + + // Check for past due warning first (highest priority) + if (this.shouldShowPastDueWarning(organizationBillingMetadata)) { + const gracePeriodEnd = this.getGracePeriodEndDate(organizationBillingMetadata.invoiceDueDate); + if (!gracePeriodEnd) { + return null; + } + return { + type: "warning", + message: this.i18nService.t( + "resellerPastDueWarningMsg", + organization.providerName, + this.formatDate(gracePeriodEnd), + ), + } as ResellerWarning; + } + + // Check for open invoice warning + if (this.shouldShowInvoiceWarning(organizationBillingMetadata)) { + const invoiceCreatedDate = organizationBillingMetadata.invoiceCreatedDate; + const invoiceDueDate = organizationBillingMetadata.invoiceDueDate; + if (!invoiceCreatedDate || !invoiceDueDate) { + return null; + } + return { + type: "info", + message: this.i18nService.t( + "resellerOpenInvoiceWarningMgs", + organization.providerName, + this.formatDate(organizationBillingMetadata.invoiceCreatedDate), + this.formatDate(organizationBillingMetadata.invoiceDueDate), + ), + } as ResellerWarning; + } + + // Check for renewal warning + if (this.shouldShowRenewalWarning(organizationBillingMetadata)) { + const subPeriodEndDate = organizationBillingMetadata.subPeriodEndDate; + if (!subPeriodEndDate) { + return null; + } + + return { + type: "info", + message: this.i18nService.t( + "resellerRenewalWarningMsg", + organization.providerName, + this.formatDate(organizationBillingMetadata.subPeriodEndDate), + ), + } as ResellerWarning; + } + + return null; + } + + private shouldShowRenewalWarning( + organizationBillingMetadata: OrganizationBillingMetadataResponse, + ): boolean { + if ( + !organizationBillingMetadata.hasSubscription || + !organizationBillingMetadata.subPeriodEndDate + ) { + return false; + } + const renewalDate = new Date(organizationBillingMetadata.subPeriodEndDate); + const daysUntilRenewal = Math.ceil( + (renewalDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24), + ); + return daysUntilRenewal <= this.RENEWAL_WARNING_DAYS; + } + + private shouldShowInvoiceWarning( + organizationBillingMetadata: OrganizationBillingMetadataResponse, + ): boolean { + if ( + !organizationBillingMetadata.hasOpenInvoice || + !organizationBillingMetadata.invoiceDueDate + ) { + return false; + } + const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate); + return invoiceDueDate > new Date(); + } + + private shouldShowPastDueWarning( + organizationBillingMetadata: OrganizationBillingMetadataResponse, + ): boolean { + if ( + !organizationBillingMetadata.hasOpenInvoice || + !organizationBillingMetadata.invoiceDueDate + ) { + return false; + } + const invoiceDueDate = new Date(organizationBillingMetadata.invoiceDueDate); + return invoiceDueDate <= new Date() && !organizationBillingMetadata.isSubscriptionUnpaid; + } + + private getGracePeriodEndDate(dueDate: Date | null): Date | null { + if (!dueDate) { + return null; + } + const gracePeriodEnd = new Date(dueDate); + gracePeriodEnd.setDate(gracePeriodEnd.getDate() + this.GRACE_PERIOD_DAYS); + return gracePeriodEnd; + } + + private formatDate(date: Date | null): string { + if (!date) { + return "N/A"; + } + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }); + } +} diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts index 558851ad64c..a3a4ba6bba1 100644 --- a/apps/web/src/app/billing/services/trial-flow.service.ts +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -2,25 +2,37 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; +import { lastValueFrom } from "rxjs"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +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 { DialogService } from "@bitwarden/components"; import { FreeTrial } from "../../core/types/free-trial"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../organizations/change-plan-dialog.component"; @Injectable({ providedIn: "root" }) export class TrialFlowService { + private resellerManagedOrgAlert: boolean; + constructor( private i18nService: I18nService, protected dialogService: DialogService, private router: Router, protected billingApiService: BillingApiServiceAbstraction, + private organizationApiService: OrganizationApiServiceAbstraction, + private configService: ConfigService, ) {} checkForOrgsWithUpcomingPaymentIssues( organization: Organization, @@ -66,16 +78,31 @@ export class TrialFlowService { org: Organization, organizationBillingMetadata: OrganizationBillingMetadataResponse, ): Promise { - if (organizationBillingMetadata.isSubscriptionUnpaid) { - const confirmed = await this.promptForPaymentNavigation(org); + if ( + organizationBillingMetadata.isSubscriptionUnpaid || + organizationBillingMetadata.isSubscriptionCanceled + ) { + const confirmed = await this.promptForPaymentNavigation( + org, + organizationBillingMetadata.isSubscriptionCanceled, + organizationBillingMetadata.isSubscriptionUnpaid, + ); if (confirmed) { await this.navigateToPaymentMethod(org?.id); } } } - private async promptForPaymentNavigation(org: Organization): Promise { - if (!org?.isOwner) { + private async promptForPaymentNavigation( + org: Organization, + isCanceled: boolean, + isUnpaid: boolean, + ): Promise { + this.resellerManagedOrgAlert = await this.configService.getFeatureFlag( + FeatureFlag.ResellerManagedOrgAlert, + ); + + if (!org?.isOwner && !org.providerId) { await this.dialogService.openSimpleDialog({ title: this.i18nService.t("suspendedOrganizationTitle", org?.name), content: { key: "suspendedUserOrgMessage" }, @@ -85,13 +112,31 @@ export class TrialFlowService { }); return false; } - return await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org?.name), - content: { key: "suspendedOwnerOrgMessage" }, - type: "danger", - acceptButtonText: this.i18nService.t("continue"), - cancelButtonText: this.i18nService.t("close"), - }); + + if (org.providerId && this.resellerManagedOrgAlert) { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org.name), + content: { key: "suspendedManagedOrgMessage", placeholders: [org.providerName] }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + return false; + } + + if (org.isOwner && isUnpaid) { + return await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + } + + if (org.isOwner && isCanceled && this.resellerManagedOrgAlert) { + await this.changePlan(org); + } } private async navigateToPaymentMethod(orgId: string) { @@ -99,4 +144,20 @@ export class TrialFlowService { state: { launchPaymentModalAutomatically: true }, }); } + + private async changePlan(org: Organization) { + const subscription = await this.organizationApiService.getSubscription(org.id); + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: org.id, + subscription: subscription, + productTierType: org.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + if (result === ChangePlanDialogResultType.Closed) { + return; + } + } } diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html index e41d3d961cd..bb06f87ca03 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog-v2.component.html @@ -5,7 +5,12 @@ [showBankAccount]="!!organizationId" [initialPaymentMethod]="initialPaymentMethod" > - + - - diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 298573f0852..149b4adf520 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { lastValueFrom } from "rxjs"; @@ -16,7 +16,6 @@ import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/mode import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { VerifyBankRequest } from "@bitwarden/common/models/request/verify-bank.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -29,15 +28,12 @@ import { AdjustPaymentDialogResult, openAdjustPaymentDialog, } from "./adjust-payment-dialog/adjust-payment-dialog.component"; -import { TaxInfoComponent } from "./tax-info.component"; @Component({ templateUrl: "payment-method.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class PaymentMethodComponent implements OnInit, OnDestroy { - @ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent; - loading = false; firstLoaded = false; billing: BillingPaymentResponse; @@ -61,7 +57,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { ]), }); - taxForm = this.formBuilder.group({}); launchPaymentModalAutomatically = false; protected freeTrialData: FreeTrial; @@ -72,7 +67,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { protected platformUtilsService: PlatformUtilsService, private router: Router, private location: Location, - private logService: LogService, private route: ActivatedRoute, private formBuilder: FormBuilder, private dialogService: DialogService, @@ -198,15 +192,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { await this.load(); }; - submitTaxInfo = async () => { - await this.taxInfo.submitTaxInfo(); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("taxInfoUpdated"), - }); - }; - determineOrgsWithUpcomingPaymentIssues() { this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( this.organization, @@ -231,10 +216,6 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { return this.organizationId != null; } - get headerClass() { - return this.forOrganization ? ["page-header"] : ["tabbed-header"]; - } - get paymentSourceClasses() { if (this.paymentSource == null) { return []; diff --git a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts index 10cf7ccb702..f65a5743c35 100644 --- a/apps/web/src/app/billing/shared/payment/payment-v2.component.ts +++ b/apps/web/src/app/billing/shared/payment/payment-v2.component.ts @@ -113,16 +113,21 @@ export class PaymentV2Component implements OnInit, OnDestroy { const clientSecret = await this.billingApiService.createSetupIntent(type); if (this.usingBankAccount) { - const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { - accountHolderName: this.formGroup.value.bankInformation.accountHolderName, - routingNumber: this.formGroup.value.bankInformation.routingNumber, - accountNumber: this.formGroup.value.bankInformation.accountNumber, - accountHolderType: this.formGroup.value.bankInformation.accountHolderType, - }); - return { - type, - token, - }; + this.formGroup.markAllAsTouched(); + if (this.formGroup.valid) { + const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, { + accountHolderName: this.formGroup.value.bankInformation.accountHolderName, + routingNumber: this.formGroup.value.bankInformation.routingNumber, + accountNumber: this.formGroup.value.bankInformation.accountNumber, + accountHolderType: this.formGroup.value.bankInformation.accountHolderType, + }); + return { + type, + token, + }; + } else { + throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again."; + } } if (this.usingCard) { @@ -142,6 +147,13 @@ export class PaymentV2Component implements OnInit, OnDestroy { }; } + if (this.usingAccountCredit) { + return { + type: PaymentMethodType.Credit, + token: null, + }; + } + return null; } diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html index 82d5104a53a..4a42c0c1109 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ b/apps/web/src/app/billing/shared/tax-info.component.html @@ -13,51 +13,41 @@
    -
    +
    {{ "zipPostalCode" | i18n }}
    -
    - - - {{ "includeVAT" | i18n }} - +
    + + {{ "address1" | i18n }} + +
    -
    -
    -
    +
    + + {{ "address2" | i18n }} + + +
    +
    + + {{ "cityTown" | i18n }} + + +
    +
    + + {{ "stateProvince" | i18n }} + + +
    +
    {{ "taxIdNumber" | i18n }}
    -
    -
    - - {{ "address1" | i18n }} - - -
    -
    - - {{ "address2" | i18n }} - - -
    -
    - - {{ "cityTown" | i18n }} - - -
    -
    - - {{ "stateProvince" | i18n }} - - -
    -
    diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts index 8ebec5e1dfe..214364e4cf2 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ b/apps/web/src/app/billing/shared/tax-info.component.ts @@ -1,31 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; +import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { CountryListItem } from "@bitwarden/common/billing/models/domain"; import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { TaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/tax-info-update.request"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; -import { TaxRateResponse } from "@bitwarden/common/billing/models/response/tax-rate.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SharedModule } from "../../shared"; -type TaxInfoView = Omit & { - includeTaxId: boolean; - [key: string]: unknown; -}; - -type CountryList = { - name: string; - value: string; - disabled: boolean; -}; - @Component({ selector: "app-tax-info", templateUrl: "tax-info.component.html", @@ -33,359 +22,68 @@ type CountryList = { imports: [SharedModule], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TaxInfoComponent implements OnInit { - @Input() trialFlow = false; - @Output() onCountryChanged = new EventEmitter(); +export class TaxInfoComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); + @Input() trialFlow = false; + @Output() countryChanged = new EventEmitter(); + @Output() taxInformationChanged: EventEmitter = new EventEmitter(); + taxFormGroup = new FormGroup({ - country: new FormControl(null, [Validators.required]), - postalCode: new FormControl(null), - includeTaxId: new FormControl(null), - taxId: new FormControl(null), - line1: new FormControl(null), - line2: new FormControl(null), - city: new FormControl(null), - state: new FormControl(null), + country: new FormControl(null, [Validators.required]), + postalCode: new FormControl(null, [Validators.required]), + taxId: new FormControl(null), + line1: new FormControl(null), + line2: new FormControl(null), + city: new FormControl(null), + state: new FormControl(null), }); + protected isTaxSupported: boolean; + loading = true; organizationId: string; providerId: string; - taxInfo: TaxInfoView = { - taxId: null, - line1: null, - line2: null, - city: null, - state: null, - postalCode: null, - country: "US", - includeTaxId: false, - }; - countryList: CountryList[] = [ - { name: "-- Select --", value: "", disabled: false }, - { name: "United States", value: "US", disabled: false }, - { name: "China", value: "CN", disabled: false }, - { name: "France", value: "FR", disabled: false }, - { name: "Germany", value: "DE", disabled: false }, - { name: "Canada", value: "CA", disabled: false }, - { name: "United Kingdom", value: "GB", disabled: false }, - { name: "Australia", value: "AU", disabled: false }, - { name: "India", value: "IN", disabled: false }, - { name: "", value: "-", disabled: true }, - { name: "Afghanistan", value: "AF", disabled: false }, - { name: "Åland Islands", value: "AX", disabled: false }, - { name: "Albania", value: "AL", disabled: false }, - { name: "Algeria", value: "DZ", disabled: false }, - { name: "American Samoa", value: "AS", disabled: false }, - { name: "Andorra", value: "AD", disabled: false }, - { name: "Angola", value: "AO", disabled: false }, - { name: "Anguilla", value: "AI", disabled: false }, - { name: "Antarctica", value: "AQ", disabled: false }, - { name: "Antigua and Barbuda", value: "AG", disabled: false }, - { name: "Argentina", value: "AR", disabled: false }, - { name: "Armenia", value: "AM", disabled: false }, - { name: "Aruba", value: "AW", disabled: false }, - { name: "Austria", value: "AT", disabled: false }, - { name: "Azerbaijan", value: "AZ", disabled: false }, - { name: "Bahamas", value: "BS", disabled: false }, - { name: "Bahrain", value: "BH", disabled: false }, - { name: "Bangladesh", value: "BD", disabled: false }, - { name: "Barbados", value: "BB", disabled: false }, - { name: "Belarus", value: "BY", disabled: false }, - { name: "Belgium", value: "BE", disabled: false }, - { name: "Belize", value: "BZ", disabled: false }, - { name: "Benin", value: "BJ", disabled: false }, - { name: "Bermuda", value: "BM", disabled: false }, - { name: "Bhutan", value: "BT", disabled: false }, - { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, - { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, - { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, - { name: "Botswana", value: "BW", disabled: false }, - { name: "Bouvet Island", value: "BV", disabled: false }, - { name: "Brazil", value: "BR", disabled: false }, - { name: "British Indian Ocean Territory", value: "IO", disabled: false }, - { name: "Brunei Darussalam", value: "BN", disabled: false }, - { name: "Bulgaria", value: "BG", disabled: false }, - { name: "Burkina Faso", value: "BF", disabled: false }, - { name: "Burundi", value: "BI", disabled: false }, - { name: "Cambodia", value: "KH", disabled: false }, - { name: "Cameroon", value: "CM", disabled: false }, - { name: "Cape Verde", value: "CV", disabled: false }, - { name: "Cayman Islands", value: "KY", disabled: false }, - { name: "Central African Republic", value: "CF", disabled: false }, - { name: "Chad", value: "TD", disabled: false }, - { name: "Chile", value: "CL", disabled: false }, - { name: "Christmas Island", value: "CX", disabled: false }, - { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, - { name: "Colombia", value: "CO", disabled: false }, - { name: "Comoros", value: "KM", disabled: false }, - { name: "Congo", value: "CG", disabled: false }, - { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, - { name: "Cook Islands", value: "CK", disabled: false }, - { name: "Costa Rica", value: "CR", disabled: false }, - { name: "Côte d'Ivoire", value: "CI", disabled: false }, - { name: "Croatia", value: "HR", disabled: false }, - { name: "Cuba", value: "CU", disabled: false }, - { name: "Curaçao", value: "CW", disabled: false }, - { name: "Cyprus", value: "CY", disabled: false }, - { name: "Czech Republic", value: "CZ", disabled: false }, - { name: "Denmark", value: "DK", disabled: false }, - { name: "Djibouti", value: "DJ", disabled: false }, - { name: "Dominica", value: "DM", disabled: false }, - { name: "Dominican Republic", value: "DO", disabled: false }, - { name: "Ecuador", value: "EC", disabled: false }, - { name: "Egypt", value: "EG", disabled: false }, - { name: "El Salvador", value: "SV", disabled: false }, - { name: "Equatorial Guinea", value: "GQ", disabled: false }, - { name: "Eritrea", value: "ER", disabled: false }, - { name: "Estonia", value: "EE", disabled: false }, - { name: "Ethiopia", value: "ET", disabled: false }, - { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, - { name: "Faroe Islands", value: "FO", disabled: false }, - { name: "Fiji", value: "FJ", disabled: false }, - { name: "Finland", value: "FI", disabled: false }, - { name: "French Guiana", value: "GF", disabled: false }, - { name: "French Polynesia", value: "PF", disabled: false }, - { name: "French Southern Territories", value: "TF", disabled: false }, - { name: "Gabon", value: "GA", disabled: false }, - { name: "Gambia", value: "GM", disabled: false }, - { name: "Georgia", value: "GE", disabled: false }, - { name: "Ghana", value: "GH", disabled: false }, - { name: "Gibraltar", value: "GI", disabled: false }, - { name: "Greece", value: "GR", disabled: false }, - { name: "Greenland", value: "GL", disabled: false }, - { name: "Grenada", value: "GD", disabled: false }, - { name: "Guadeloupe", value: "GP", disabled: false }, - { name: "Guam", value: "GU", disabled: false }, - { name: "Guatemala", value: "GT", disabled: false }, - { name: "Guernsey", value: "GG", disabled: false }, - { name: "Guinea", value: "GN", disabled: false }, - { name: "Guinea-Bissau", value: "GW", disabled: false }, - { name: "Guyana", value: "GY", disabled: false }, - { name: "Haiti", value: "HT", disabled: false }, - { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, - { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, - { name: "Honduras", value: "HN", disabled: false }, - { name: "Hong Kong", value: "HK", disabled: false }, - { name: "Hungary", value: "HU", disabled: false }, - { name: "Iceland", value: "IS", disabled: false }, - { name: "Indonesia", value: "ID", disabled: false }, - { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, - { name: "Iraq", value: "IQ", disabled: false }, - { name: "Ireland", value: "IE", disabled: false }, - { name: "Isle of Man", value: "IM", disabled: false }, - { name: "Israel", value: "IL", disabled: false }, - { name: "Italy", value: "IT", disabled: false }, - { name: "Jamaica", value: "JM", disabled: false }, - { name: "Japan", value: "JP", disabled: false }, - { name: "Jersey", value: "JE", disabled: false }, - { name: "Jordan", value: "JO", disabled: false }, - { name: "Kazakhstan", value: "KZ", disabled: false }, - { name: "Kenya", value: "KE", disabled: false }, - { name: "Kiribati", value: "KI", disabled: false }, - { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, - { name: "Korea, Republic of", value: "KR", disabled: false }, - { name: "Kuwait", value: "KW", disabled: false }, - { name: "Kyrgyzstan", value: "KG", disabled: false }, - { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, - { name: "Latvia", value: "LV", disabled: false }, - { name: "Lebanon", value: "LB", disabled: false }, - { name: "Lesotho", value: "LS", disabled: false }, - { name: "Liberia", value: "LR", disabled: false }, - { name: "Libya", value: "LY", disabled: false }, - { name: "Liechtenstein", value: "LI", disabled: false }, - { name: "Lithuania", value: "LT", disabled: false }, - { name: "Luxembourg", value: "LU", disabled: false }, - { name: "Macao", value: "MO", disabled: false }, - { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, - { name: "Madagascar", value: "MG", disabled: false }, - { name: "Malawi", value: "MW", disabled: false }, - { name: "Malaysia", value: "MY", disabled: false }, - { name: "Maldives", value: "MV", disabled: false }, - { name: "Mali", value: "ML", disabled: false }, - { name: "Malta", value: "MT", disabled: false }, - { name: "Marshall Islands", value: "MH", disabled: false }, - { name: "Martinique", value: "MQ", disabled: false }, - { name: "Mauritania", value: "MR", disabled: false }, - { name: "Mauritius", value: "MU", disabled: false }, - { name: "Mayotte", value: "YT", disabled: false }, - { name: "Mexico", value: "MX", disabled: false }, - { name: "Micronesia, Federated States of", value: "FM", disabled: false }, - { name: "Moldova, Republic of", value: "MD", disabled: false }, - { name: "Monaco", value: "MC", disabled: false }, - { name: "Mongolia", value: "MN", disabled: false }, - { name: "Montenegro", value: "ME", disabled: false }, - { name: "Montserrat", value: "MS", disabled: false }, - { name: "Morocco", value: "MA", disabled: false }, - { name: "Mozambique", value: "MZ", disabled: false }, - { name: "Myanmar", value: "MM", disabled: false }, - { name: "Namibia", value: "NA", disabled: false }, - { name: "Nauru", value: "NR", disabled: false }, - { name: "Nepal", value: "NP", disabled: false }, - { name: "Netherlands", value: "NL", disabled: false }, - { name: "New Caledonia", value: "NC", disabled: false }, - { name: "New Zealand", value: "NZ", disabled: false }, - { name: "Nicaragua", value: "NI", disabled: false }, - { name: "Niger", value: "NE", disabled: false }, - { name: "Nigeria", value: "NG", disabled: false }, - { name: "Niue", value: "NU", disabled: false }, - { name: "Norfolk Island", value: "NF", disabled: false }, - { name: "Northern Mariana Islands", value: "MP", disabled: false }, - { name: "Norway", value: "NO", disabled: false }, - { name: "Oman", value: "OM", disabled: false }, - { name: "Pakistan", value: "PK", disabled: false }, - { name: "Palau", value: "PW", disabled: false }, - { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, - { name: "Panama", value: "PA", disabled: false }, - { name: "Papua New Guinea", value: "PG", disabled: false }, - { name: "Paraguay", value: "PY", disabled: false }, - { name: "Peru", value: "PE", disabled: false }, - { name: "Philippines", value: "PH", disabled: false }, - { name: "Pitcairn", value: "PN", disabled: false }, - { name: "Poland", value: "PL", disabled: false }, - { name: "Portugal", value: "PT", disabled: false }, - { name: "Puerto Rico", value: "PR", disabled: false }, - { name: "Qatar", value: "QA", disabled: false }, - { name: "Réunion", value: "RE", disabled: false }, - { name: "Romania", value: "RO", disabled: false }, - { name: "Russian Federation", value: "RU", disabled: false }, - { name: "Rwanda", value: "RW", disabled: false }, - { name: "Saint Barthélemy", value: "BL", disabled: false }, - { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, - { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, - { name: "Saint Lucia", value: "LC", disabled: false }, - { name: "Saint Martin (French part)", value: "MF", disabled: false }, - { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, - { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, - { name: "Samoa", value: "WS", disabled: false }, - { name: "San Marino", value: "SM", disabled: false }, - { name: "Sao Tome and Principe", value: "ST", disabled: false }, - { name: "Saudi Arabia", value: "SA", disabled: false }, - { name: "Senegal", value: "SN", disabled: false }, - { name: "Serbia", value: "RS", disabled: false }, - { name: "Seychelles", value: "SC", disabled: false }, - { name: "Sierra Leone", value: "SL", disabled: false }, - { name: "Singapore", value: "SG", disabled: false }, - { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, - { name: "Slovakia", value: "SK", disabled: false }, - { name: "Slovenia", value: "SI", disabled: false }, - { name: "Solomon Islands", value: "SB", disabled: false }, - { name: "Somalia", value: "SO", disabled: false }, - { name: "South Africa", value: "ZA", disabled: false }, - { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, - { name: "South Sudan", value: "SS", disabled: false }, - { name: "Spain", value: "ES", disabled: false }, - { name: "Sri Lanka", value: "LK", disabled: false }, - { name: "Sudan", value: "SD", disabled: false }, - { name: "Suriname", value: "SR", disabled: false }, - { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, - { name: "Swaziland", value: "SZ", disabled: false }, - { name: "Sweden", value: "SE", disabled: false }, - { name: "Switzerland", value: "CH", disabled: false }, - { name: "Syrian Arab Republic", value: "SY", disabled: false }, - { name: "Taiwan", value: "TW", disabled: false }, - { name: "Tajikistan", value: "TJ", disabled: false }, - { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, - { name: "Thailand", value: "TH", disabled: false }, - { name: "Timor-Leste", value: "TL", disabled: false }, - { name: "Togo", value: "TG", disabled: false }, - { name: "Tokelau", value: "TK", disabled: false }, - { name: "Tonga", value: "TO", disabled: false }, - { name: "Trinidad and Tobago", value: "TT", disabled: false }, - { name: "Tunisia", value: "TN", disabled: false }, - { name: "Turkey", value: "TR", disabled: false }, - { name: "Turkmenistan", value: "TM", disabled: false }, - { name: "Turks and Caicos Islands", value: "TC", disabled: false }, - { name: "Tuvalu", value: "TV", disabled: false }, - { name: "Uganda", value: "UG", disabled: false }, - { name: "Ukraine", value: "UA", disabled: false }, - { name: "United Arab Emirates", value: "AE", disabled: false }, - { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, - { name: "Uruguay", value: "UY", disabled: false }, - { name: "Uzbekistan", value: "UZ", disabled: false }, - { name: "Vanuatu", value: "VU", disabled: false }, - { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, - { name: "Viet Nam", value: "VN", disabled: false }, - { name: "Virgin Islands, British", value: "VG", disabled: false }, - { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, - { name: "Wallis and Futuna", value: "WF", disabled: false }, - { name: "Western Sahara", value: "EH", disabled: false }, - { name: "Yemen", value: "YE", disabled: false }, - { name: "Zambia", value: "ZM", disabled: false }, - { name: "Zimbabwe", value: "ZW", disabled: false }, - ]; - taxRates: TaxRateResponse[]; + countryList: CountryListItem[] = this.taxService.getCountries(); constructor( private apiService: ApiService, private route: ActivatedRoute, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, + private taxService: TaxServiceAbstraction, ) {} get country(): string { - return this.taxFormGroup.get("country").value; - } - - set country(country: string) { - this.taxFormGroup.get("country").setValue(country); + return this.taxFormGroup.controls.country.value; } get postalCode(): string { - return this.taxFormGroup.get("postalCode").value; - } - - set postalCode(postalCode: string) { - this.taxFormGroup.get("postalCode").setValue(postalCode); - } - - get includeTaxId(): boolean { - return this.taxFormGroup.get("includeTaxId").value; - } - - set includeTaxId(includeTaxId: boolean) { - this.taxFormGroup.get("includeTaxId").setValue(includeTaxId); + return this.taxFormGroup.controls.postalCode.value; } get taxId(): string { - return this.taxFormGroup.get("taxId").value; - } - - set taxId(taxId: string) { - this.taxFormGroup.get("taxId").setValue(taxId); + return this.taxFormGroup.controls.taxId.value; } get line1(): string { - return this.taxFormGroup.get("line1").value; - } - - set line1(line1: string) { - this.taxFormGroup.get("line1").setValue(line1); + return this.taxFormGroup.controls.line1.value; } get line2(): string { - return this.taxFormGroup.get("line2").value; - } - - set line2(line2: string) { - this.taxFormGroup.get("line2").setValue(line2); + return this.taxFormGroup.controls.line2.value; } get city(): string { - return this.taxFormGroup.get("city").value; - } - - set city(city: string) { - this.taxFormGroup.get("city").setValue(city); + return this.taxFormGroup.controls.city.value; } get state(): string { - return this.taxFormGroup.get("state").value; + return this.taxFormGroup.controls.state.value; } - set state(state: string) { - this.taxFormGroup.get("state").setValue(state); + get showTaxIdField(): boolean { + return !!this.organizationId; } async ngOnInit() { @@ -402,22 +100,13 @@ export class TaxInfoComponent implements OnInit { try { const taxInfo = await this.organizationApiService.getTaxInfo(this.organizationId); if (taxInfo) { - this.taxId = taxInfo.taxId; - this.state = taxInfo.state; - this.line1 = taxInfo.line1; - this.line2 = taxInfo.line2; - this.city = taxInfo.city; - this.state = taxInfo.state; - this.postalCode = taxInfo.postalCode; - this.country = taxInfo.country || "US"; - this.includeTaxId = - this.countrySupportsTax(this.country) && - (!!taxInfo.taxId || - !!taxInfo.line1 || - !!taxInfo.line2 || - !!taxInfo.city || - !!taxInfo.state); - this.setTaxInfoObject(); + this.taxFormGroup.controls.taxId.setValue(taxInfo.taxId); + this.taxFormGroup.controls.state.setValue(taxInfo.state); + this.taxFormGroup.controls.line1.setValue(taxInfo.line1); + this.taxFormGroup.controls.line2.setValue(taxInfo.line2); + this.taxFormGroup.controls.city.setValue(taxInfo.city); + this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); + this.taxFormGroup.controls.country.setValue(taxInfo.country); } } catch (e) { this.logService.error(e); @@ -426,119 +115,79 @@ export class TaxInfoComponent implements OnInit { try { const taxInfo = await this.apiService.getTaxInfo(); if (taxInfo) { - this.postalCode = taxInfo.postalCode; - this.country = taxInfo.country || "US"; + this.taxFormGroup.controls.postalCode.setValue(taxInfo.postalCode); + this.taxFormGroup.controls.country.setValue(taxInfo.country); } - this.setTaxInfoObject(); } catch (e) { this.logService.error(e); } } - if (this.country === "US") { - this.taxFormGroup.get("postalCode").setValidators([Validators.required]); - this.taxFormGroup.get("postalCode").updateValueAndValidity(); - } + this.isTaxSupported = await this.taxService.isCountrySupported( + this.taxFormGroup.controls.country.value, + ); - if (this.country !== "US") { - this.onCountryChanged.emit(); - } + this.countryChanged.emit(); }); - this.taxFormGroup - .get("country") - .valueChanges.pipe(takeUntil(this.destroy$)) + this.taxFormGroup.controls.country.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) .subscribe((value) => { - if (value === "US") { - this.taxFormGroup.get("postalCode").setValidators([Validators.required]); - } else { - this.taxFormGroup.get("postalCode").clearValidators(); - } - this.taxFormGroup.get("postalCode").updateValueAndValidity(); - this.setTaxInfoObject(); - this.changeCountry(); + this.taxService + .isCountrySupported(this.taxFormGroup.controls.country.value) + .then((isSupported) => { + this.isTaxSupported = isSupported; + }) + .catch(() => { + this.isTaxSupported = false; + }) + .finally(() => { + if (!this.isTaxSupported) { + this.taxFormGroup.controls.taxId.setValue(null); + this.taxFormGroup.controls.line1.setValue(null); + this.taxFormGroup.controls.line2.setValue(null); + this.taxFormGroup.controls.city.setValue(null); + this.taxFormGroup.controls.state.setValue(null); + } + + this.countryChanged.emit(); + }); + this.taxInformationChanged.emit(); }); - try { - const taxRates = await this.apiService.getTaxRates(); - if (taxRates) { - this.taxRates = taxRates.data; - } - } catch (e) { - this.logService.error(e); - } finally { - this.loading = false; - } + this.taxFormGroup.controls.postalCode.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + this.taxInformationChanged.emit(); + }); + + this.taxFormGroup.controls.taxId.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + this.taxInformationChanged.emit(); + }); + + this.loading = false; } - get taxRate() { - if (this.taxRates != null) { - const localTaxRate = this.taxRates.find( - (x) => x.country === this.country && x.postalCode === this.postalCode, - ); - return localTaxRate?.rate ?? null; - } - } - - setTaxInfoObject() { - this.taxInfo.country = this.country; - this.taxInfo.postalCode = this.postalCode; - this.taxInfo.includeTaxId = this.includeTaxId; - this.taxInfo.taxId = this.taxId; - this.taxInfo.line1 = this.line1; - this.taxInfo.line2 = this.line2; - this.taxInfo.city = this.city; - this.taxInfo.state = this.state; - } - - get showTaxIdCheckbox() { - return ( - (this.organizationId || this.providerId) && - this.country !== "US" && - this.countrySupportsTax(this.taxInfo.country) - ); - } - - get showTaxIdFields() { - return ( - (this.organizationId || this.providerId) && - this.includeTaxId && - this.countrySupportsTax(this.country) - ); - } - - getTaxInfoRequest(): TaxInfoUpdateRequest { - if (this.organizationId || this.providerId) { - const request = new ExpandedTaxInfoUpdateRequest(); - request.country = this.country; - request.postalCode = this.postalCode; - - if (this.includeTaxId) { - request.taxId = this.taxId; - request.line1 = this.line1; - request.line2 = this.line2; - request.city = this.city; - request.state = this.state; - } else { - request.taxId = null; - request.line1 = null; - request.line2 = null; - request.city = null; - request.state = null; - } - return request; - } else { - const request = new TaxInfoUpdateRequest(); - request.postalCode = this.postalCode; - request.country = this.country; - return request; - } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } submitTaxInfo(): Promise { this.taxFormGroup.updateValueAndValidity(); this.taxFormGroup.markAllAsTouched(); - const request = this.getTaxInfoRequest(); + + const request = new ExpandedTaxInfoUpdateRequest(); + request.country = this.country; + request.postalCode = this.postalCode; + request.taxId = this.taxId; + request.line1 = this.line1; + request.line2 = this.line2; + request.city = this.city; + request.state = this.state; + return this.organizationId ? this.organizationApiService.updateTaxInfo( this.organizationId, @@ -546,97 +195,4 @@ export class TaxInfoComponent implements OnInit { ) : this.apiService.putTaxInfo(request); } - - changeCountry() { - if (!this.countrySupportsTax(this.country)) { - this.includeTaxId = false; - this.taxId = null; - this.line1 = null; - this.line2 = null; - this.city = null; - this.state = null; - this.setTaxInfoObject(); - } - this.onCountryChanged.emit(); - } - - countrySupportsTax(countryCode: string) { - return this.taxSupportedCountryCodes.includes(countryCode); - } - - private taxSupportedCountryCodes: string[] = [ - "CN", - "FR", - "DE", - "CA", - "GB", - "AU", - "IN", - "AD", - "AR", - "AT", - "BE", - "BO", - "BR", - "BG", - "CL", - "CO", - "CR", - "HR", - "CY", - "CZ", - "DK", - "DO", - "EC", - "EG", - "SV", - "EE", - "FI", - "GE", - "GR", - "HK", - "HU", - "IS", - "ID", - "IQ", - "IE", - "IL", - "IT", - "JP", - "KE", - "KR", - "LV", - "LI", - "LT", - "LU", - "MY", - "MT", - "MX", - "NL", - "NZ", - "NO", - "PE", - "PH", - "PL", - "PT", - "RO", - "RU", - "SA", - "RS", - "SG", - "SK", - "SI", - "ZA", - "ES", - "SE", - "CH", - "TW", - "TH", - "TR", - "UA", - "AE", - "UY", - "VE", - "VN", - ]; } diff --git a/apps/web/src/app/common/base.accept.component.ts b/apps/web/src/app/common/base.accept.component.ts index 4e938fcd081..82a192ab0cf 100644 --- a/apps/web/src/app/common/base.accept.component.ts +++ b/apps/web/src/app/common/base.accept.component.ts @@ -5,7 +5,6 @@ import { ActivatedRoute, Params, Router } from "@angular/router"; import { Subject, firstValueFrom } from "rxjs"; import { first, switchMap, takeUntil } from "rxjs/operators"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -22,9 +21,6 @@ export abstract class BaseAcceptComponent implements OnInit { protected failedShortMessage = "inviteAcceptFailedShort"; protected failedMessage = "inviteAcceptFailed"; - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - private destroy$ = new Subject(); constructor( @@ -33,10 +29,10 @@ export abstract class BaseAcceptComponent implements OnInit { protected i18nService: I18nService, protected route: ActivatedRoute, protected authService: AuthService, - protected registerRouteService: RegisterRouteService, ) {} abstract authedHandler(qParams: Params): Promise; + abstract unauthedHandler(qParams: Params): Promise; async ngOnInit() { diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 2dd1db9fdb6..8f21dfa2c8b 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -30,7 +30,6 @@ import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/serv import { RegistrationFinishService as RegistrationFinishServiceAbstraction, LoginComponentService, - LockComponentService, SetPasswordJitService, SsoComponentService, LoginDecryptionOptionsService, @@ -92,6 +91,7 @@ import { KeyService as KeyServiceAbstraction, BiometricsService, } from "@bitwarden/key-management"; +import { LockComponentService } from "@bitwarden/key-management/angular"; import { flagEnabled } from "../../utils/flags"; import { PolicyListService } from "../admin-console/core/policy-list.service"; @@ -99,13 +99,13 @@ import { WebSetPasswordJitService, WebRegistrationFinishService, WebLoginComponentService, - WebLockComponentService, WebLoginDecryptionOptionsService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; +import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service"; import { WebProcessReloadService } from "../key-management/services/web-process-reload.service"; import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; diff --git a/apps/web/src/app/core/guards/has-premium.guard.ts b/apps/web/src/app/core/guards/has-premium.guard.ts index ab544dafb61..61853b25cb8 100644 --- a/apps/web/src/app/core/guards/has-premium.guard.ts +++ b/apps/web/src/app/core/guards/has-premium.guard.ts @@ -6,9 +6,10 @@ import { CanActivateFn, UrlTree, } from "@angular/router"; -import { Observable } from "rxjs"; -import { tap } from "rxjs/operators"; +import { Observable, of } from "rxjs"; +import { switchMap, tap } from "rxjs/operators"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -24,8 +25,14 @@ export function hasPremiumGuard(): CanActivateFn { const router = inject(Router); const messagingService = inject(MessagingService); const billingAccountProfileStateService = inject(BillingAccountProfileStateService); + const accountService = inject(AccountService); - return billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( + return accountService.activeAccount$.pipe( + switchMap((account) => + account + ? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), tap((userHasPremium: boolean) => { if (!userHasPremium) { messagingService.send("premiumRequired"); diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts similarity index 95% rename from apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts rename to apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts index 5eb26a8c76c..3c941fe24c7 100644 --- a/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts @@ -4,6 +4,7 @@ import { firstValueFrom, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus } from "@bitwarden/key-management"; import { WebLockComponentService } from "./web-lock-component.service"; @@ -86,7 +87,7 @@ describe("WebLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: null, + biometricsStatus: BiometricsStatus.PlatformUnsupported, }, }); }); diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts similarity index 90% rename from apps/web/src/app/auth/core/services/web-lock-component.service.ts rename to apps/web/src/app/key-management/lock/services/web-lock-component.service.ts index e24f299e23b..02910966d6e 100644 --- a/apps/web/src/app/auth/core/services/web-lock-component.service.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts @@ -1,12 +1,13 @@ import { inject } from "@angular/core"; import { map, Observable } from "rxjs"; -import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular"; import { UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus } from "@bitwarden/key-management"; +import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; export class WebLockComponentService implements LockComponentService { private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); @@ -45,7 +46,7 @@ export class WebLockComponentService implements LockComponentService { }, biometrics: { enabled: false, - disableReason: null, + biometricsStatus: BiometricsStatus.PlatformUnsupported, }, }; return unlockOpts; diff --git a/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts index bb5a1c511c6..2361293fe80 100644 --- a/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/key-management/migrate-encryption/migrate-legacy-encryption.component.ts @@ -83,7 +83,7 @@ export class MigrateFromLegacyEncryptionComponent { }); if (deleteFolders) { - await this.folderApiService.deleteAll(); + await this.folderApiService.deleteAll(activeUser.id); await this.syncService.fullSync(true, true); await this.submit(); return; diff --git a/apps/web/src/app/key-management/web-biometric.service.ts b/apps/web/src/app/key-management/web-biometric.service.ts index 4681eb6fa49..0c58c0da759 100644 --- a/apps/web/src/app/key-management/web-biometric.service.ts +++ b/apps/web/src/app/key-management/web-biometric.service.ts @@ -1,27 +1,27 @@ -import { BiometricsService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management"; export class WebBiometricsService extends BiometricsService { - async supportsBiometric(): Promise { + async authenticateWithBiometrics(): Promise { return false; } - async isBiometricUnlockAvailable(): Promise { + async getBiometricsStatus(): Promise { + return BiometricsStatus.PlatformUnsupported; + } + + async unlockWithBiometricsForUser(userId: UserId): Promise { + return null; + } + + async getBiometricsStatusForUser(userId: UserId): Promise { + return BiometricsStatus.PlatformUnsupported; + } + + async getShouldAutopromptNow(): Promise { return false; } - async authenticateBiometric(): Promise { - throw new Error("Method not implemented."); - } - - async biometricsNeedsSetup(): Promise { - throw new Error("Method not implemented."); - } - - async biometricsSupportsAutoSetup(): Promise { - throw new Error("Method not implemented."); - } - - async biometricsSetup(): Promise { - throw new Error("Method not implemented."); - } + async setShouldAutopromptNow(value: boolean): Promise {} } diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index a7ff50b4264..1c15f7cc7c1 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -157,7 +157,6 @@ export const SMAvailable: Story = { canManageUsers: false, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -173,7 +172,6 @@ export const SMAndACAvailable: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -189,7 +187,6 @@ export const WithAllOptions: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [{ id: "provider-a" }] as Provider[], diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index b53d0243f64..7a4df4bad00 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -176,7 +176,6 @@ export const WithSM: Story = { canManageUsers: false, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -192,7 +191,6 @@ export const WithSMAndAC: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [], @@ -208,7 +206,6 @@ export const WithAllOptions: Story = { canManageUsers: true, canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => false, }, ] as Organization[], mockProviders: [{ id: "provider-a" }] as Provider[], diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts index a071d0f8852..919b3be0424 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.spec.ts @@ -116,7 +116,6 @@ describe("ProductSwitcherService", () => { id: "1234", canAccessSecretsManager: true, enabled: true, - canAccessExport: (_) => true, }, ] as Organization[]); @@ -232,14 +231,12 @@ describe("ProductSwitcherService", () => { canAccessSecretsManager: true, enabled: true, name: "Org 2", - canAccessExport: (_) => true, }, { id: "4243", canAccessSecretsManager: true, enabled: true, name: "Org 32", - canAccessExport: (_) => true, }, ] as Organization[]); diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 18277abebef..f0ac3ef9b48 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -3,12 +3,11 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { Observable, concatMap, combineLatest } from "rxjs"; +import { Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { IconModule } from "@bitwarden/components"; @@ -38,35 +37,19 @@ export class UserLayoutComponent implements OnInit { protected showSubscription$: Observable; constructor( - private platformUtilsService: PlatformUtilsService, - private apiService: ApiService, private syncService: SyncService, private billingAccountProfileStateService: BillingAccountProfileStateService, - ) {} + private accountService: AccountService, + ) { + this.showSubscription$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.canViewSubscription$(account.id), + ), + ); + } async ngOnInit() { document.body.classList.remove("layout_frontend"); - await this.syncService.fullSync(false); - - // We want to hide the subscription menu for organizations that provide premium. - // Except if the user has premium personally or has a billing history. - this.showSubscription$ = combineLatest([ - this.billingAccountProfileStateService.hasPremiumPersonally$, - this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$, - ]).pipe( - concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => { - const isCloud = !this.platformUtilsService.isSelfHost(); - - let billing = null; - if (isCloud) { - // TODO: We should remove the need to call this! - billing = await this.apiService.getUserBillingHistory(); - } - - const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory; - return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory; - }), - ); } } diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index ad536110b74..8a678a3b045 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,8 +10,8 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; -import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; +import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -25,7 +25,6 @@ import { RegistrationLinkExpiredComponent, LoginComponent, LoginSecondaryContentComponent, - LockComponent, LockIcon, TwoFactorTimeoutIcon, UserLockIcon, @@ -39,14 +38,13 @@ import { VaultIcon, LoginDecryptionOptionsComponent, } from "@bitwarden/auth/angular"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { LockComponent } from "@bitwarden/key-management/angular"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, VaultIcons, } from "@bitwarden/vault"; -import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { flagEnabled, Flags } from "../utils/flags"; import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component"; @@ -71,7 +69,6 @@ import { SecurityRoutingModule } from "./auth/settings/security/security-routing import { SsoComponentV1 } from "./auth/sso-v1.component"; import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; -import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component"; import { TwoFactorAuthComponent } from "./auth/two-factor-auth.component"; import { TwoFactorComponent } from "./auth/two-factor.component"; import { UpdatePasswordComponent } from "./auth/update-password.component"; @@ -96,6 +93,18 @@ import { SendComponent } from "./tools/send/send.component"; import { VaultModule } from "./vault/individual-vault/vault.module"; const routes: Routes = [ + // These need to be placed at the top of the list prior to the root + // so that the redirectGuard does not interrupt the navigation. + { + path: "register", + redirectTo: "signup", + pathMatch: "full", + }, + { + path: "trial", + redirectTo: "signup", + pathMatch: "full", + }, { path: "", component: FrontendLayoutComponent, @@ -112,20 +121,6 @@ const routes: Routes = [ component: LoginViaWebAuthnComponent, data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties, }, - { - path: "register", - component: TrialInitiationComponent, - canActivate: [ - canAccessFeature(FeatureFlag.EmailVerification, false, "/signup", false), - unauthGuardFn(), - ], - data: { titleId: "createAccount" } satisfies RouteDataProperties, - }, - { - path: "trial", - redirectTo: "register", - pathMatch: "full", - }, { path: "set-password", component: SetPasswordComponent, @@ -347,7 +342,7 @@ const routes: Routes = [ children: [ { path: "signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationUserAddIcon, pageTitle: { @@ -372,7 +367,7 @@ const routes: Routes = [ }, { path: "finish-signup", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationLockAltIcon, titleId: "setAStrongPassword", @@ -406,7 +401,6 @@ const routes: Routes = [ }, { path: "set-password-jit", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], component: SetPasswordJitComponent, data: { pageTitle: { @@ -419,7 +413,7 @@ const routes: Routes = [ }, { path: "signup-link-expired", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], data: { pageIcon: RegistrationExpiredLinkIcon, pageTitle: { @@ -656,7 +650,7 @@ const routes: Routes = [ }, { path: "trial-initiation", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], component: CompleteTrialInitiationComponent, resolve: { pageTitle: freeTrialTextResolver, @@ -667,7 +661,7 @@ const routes: Routes = [ }, { path: "secrets-manager-trial-initiation", - canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()], + canActivate: [unauthGuardFn()], component: CompleteTrialInitiationComponent, resolve: { pageTitle: freeTrialTextResolver, diff --git a/apps/web/src/app/platform/web-sdk-client-factory.ts b/apps/web/src/app/platform/web-sdk-client-factory.ts index 2ebb2bcc10f..0dd43ecbb92 100644 --- a/apps/web/src/app/platform/web-sdk-client-factory.ts +++ b/apps/web/src/app/platform/web-sdk-client-factory.ts @@ -27,6 +27,8 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // ignore } diff --git a/apps/web/src/app/shared/components/index.ts b/apps/web/src/app/shared/components/index.ts deleted file mode 100644 index 5745a7827ff..00000000000 --- a/apps/web/src/app/shared/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./integrations/integration-card/integration-card.component"; -export * from "./integrations/integration-grid/integration-grid.component"; -export * from "./integrations/integrations.pipe"; -export * from "./integrations/models"; diff --git a/apps/web/src/app/shared/index.ts b/apps/web/src/app/shared/index.ts index f57648c0e40..7defcdedfda 100644 --- a/apps/web/src/app/shared/index.ts +++ b/apps/web/src/app/shared/index.ts @@ -1,3 +1,2 @@ export * from "./shared.module"; export * from "./loose-components.module"; -export * from "./components/index"; diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index 1b04583a395..8f44d8a4bf5 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -3,7 +3,7 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterModule } from "@angular/router"; -import { InfiniteScrollModule } from "ngx-infinite-scroll"; +import { InfiniteScrollDirective } from "ngx-infinite-scroll"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -49,7 +49,7 @@ import "./locales"; DragDropModule, FormsModule, ReactiveFormsModule, - InfiniteScrollModule, + InfiniteScrollDirective, RouterModule, JslibModule, @@ -86,7 +86,7 @@ import "./locales"; DragDropModule, FormsModule, ReactiveFormsModule, - InfiniteScrollModule, + InfiniteScrollDirective, RouterModule, JslibModule, diff --git a/apps/web/src/app/tools/reports/pages/reports-home.component.ts b/apps/web/src/app/tools/reports/pages/reports-home.component.ts index 961c24bb017..604d66f6858 100644 --- a/apps/web/src/app/tools/reports/pages/reports-home.component.ts +++ b/apps/web/src/app/tools/reports/pages/reports-home.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { reports, ReportType } from "../reports"; @@ -15,11 +16,15 @@ import { ReportEntry, ReportVariant } from "../shared"; export class ReportsHomeComponent implements OnInit { reports: ReportEntry[]; - constructor(private billingAccountProfileStateService: BillingAccountProfileStateService) {} + constructor( + private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, + ) {} async ngOnInit(): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); const userHasPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); const reportRequiresPremium = userHasPremium ? ReportVariant.Enabled diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts index 80439acd510..ccfecdd197e 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/access.component.ts @@ -5,9 +5,7 @@ import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -58,9 +56,6 @@ export class AccessComponent implements OnInit { protected formGroup = this.formBuilder.group({}); - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - private id: string; private key: string; @@ -71,8 +66,6 @@ export class AccessComponent implements OnInit { private sendApiService: SendApiService, private toastService: ToastService, private i18nService: I18nService, - private configService: ConfigService, - private registerRouteService: RegisterRouteService, private layoutWrapperDataService: AnonLayoutWrapperDataService, protected formBuilder: FormBuilder, ) {} diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.html b/apps/web/src/app/tools/send/send-access-explainer.component.html index e8090cb850c..f56b68cc299 100644 --- a/apps/web/src/app/tools/send/send-access-explainer.component.html +++ b/apps/web/src/app/tools/send/send-access-explainer.component.html @@ -10,7 +10,7 @@ >Bitwarden Send {{ "sendAccessTaglineOr" | i18n }} - {{ + {{ "sendAccessTaglineSignUp" | i18n }} {{ "sendAccessTaglineTryToday" | i18n }} diff --git a/apps/web/src/app/tools/send/send-access-explainer.component.ts b/apps/web/src/app/tools/send/send-access-explainer.component.ts index 756a1068985..e7106110a65 100644 --- a/apps/web/src/app/tools/send/send-access-explainer.component.ts +++ b/apps/web/src/app/tools/send/send-access-explainer.component.ts @@ -1,7 +1,5 @@ import { Component } from "@angular/core"; -import { RegisterRouteService } from "@bitwarden/auth/common"; - import { SharedModule } from "../../shared"; @Component({ @@ -11,7 +9,5 @@ import { SharedModule } from "../../shared"; imports: [SharedModule], }) export class SendAccessExplainerComponent { - // TODO: remove when email verification flag is removed - registerRoute$ = this.registerRouteService.registerRoute$(); - constructor(private registerRouteService: RegisterRouteService) {} + constructor() {} } diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access-file.component.ts index 0b2a971bbe8..b55e955f355 100644 --- a/apps/web/src/app/tools/send/send-access-file.component.ts +++ b/apps/web/src/app/tools/send/send-access-file.component.ts @@ -70,6 +70,8 @@ export class SendAccessFileComponent { blobData: decBuf, downloadMethod: "save", }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.toastService.showToast({ variant: "error", diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 42d033dc4c2..6141e983a68 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -29,7 +29,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { BitValidators, DialogService } from "@bitwarden/components"; +import { BitValidators, DialogService, ToastService } from "@bitwarden/components"; import { GroupApiService, GroupView } from "../../../admin-console/organizations/core"; import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component"; @@ -110,6 +110,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private organizationUserApiService: OrganizationUserApiService, private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, + private toastService: ToastService, ) { this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; } @@ -274,17 +275,20 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { const accessTabError = this.formGroup.controls.access.hasError("managePermissionRequired"); if (this.tabIndex === CollectionDialogTabType.Access && !accessTabError) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t( + "fieldOnTabRequiresAttention", + this.i18nService.t("collectionInfo"), + ), + }); } else if (this.tabIndex === CollectionDialogTabType.Info && accessTabError) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access")), + }); } return; } @@ -309,14 +313,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { const savedCollection = await this.collectionAdminService.save(collectionView); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t( + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( this.editMode ? "editedCollectionId" : "createdCollectionId", collectionView.name, ), - ); + }); this.close(CollectionDialogAction.Saved, savedCollection); }; @@ -339,11 +343,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { await this.collectionAdminService.delete(this.params.organizationId, this.params.collectionId); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedCollectionId", this.collection?.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedCollectionId", this.collection?.name), + }); this.close(CollectionDialogAction.Deleted, this.collection); }; diff --git a/apps/web/src/app/vault/components/premium-badge.stories.ts b/apps/web/src/app/vault/components/premium-badge.stories.ts index 17622dbbd5f..331f72fd0ac 100644 --- a/apps/web/src/app/vault/components/premium-badge.stories.ts +++ b/apps/web/src/app/vault/components/premium-badge.stories.ts @@ -2,6 +2,7 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessageSender } from "@bitwarden/common/platform/messaging"; @@ -22,6 +23,14 @@ export default { moduleMetadata({ imports: [JslibModule, BadgeModule], providers: [ + { + provide: AccountService, + useValue: { + activeAccount$: of({ + id: "123", + }), + }, + }, { provide: I18nService, useFactory: () => { @@ -39,7 +48,7 @@ export default { { provide: BillingAccountProfileStateService, useValue: { - hasPremiumFromAnySource$: of(false), + hasPremiumFromAnySource$: () => of(false), }, }, ], diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html index 56acc421de4..c19329f7121 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.html @@ -36,7 +36,10 @@
    - + + @@ -62,7 +65,7 @@ type="button" buttonType="secondary" (click)="cancel()" - *ngIf="!showCipherView" + *ngIf="!showCipherView && !showRestore" > {{ "cancel" | i18n }} diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 4c1b51a5482..a530fd0cc85 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -4,14 +4,16 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, Observable, Subject } from "rxjs"; +import { firstValueFrom, Observable, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -38,6 +40,7 @@ import { CipherFormGenerationService, CipherFormModule, CipherViewComponent, + DecryptionFailureDialogComponent, } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; @@ -46,6 +49,8 @@ import { AttachmentDialogResult, AttachmentsV2Component, } from "../../individual-vault/attachments-v2.component"; +import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service"; +import { RoutedVaultFilterModel } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { WebCipherFormGenerationService } from "../../services/web-cipher-form-generation.service"; import { WebVaultPremiumUpgradePromptService } from "../../services/web-premium-upgrade-prompt.service"; import { WebViewPasswordHistoryService } from "../../services/web-view-password-history.service"; @@ -79,6 +84,11 @@ export interface VaultItemDialogParams { * If true, the dialog is being opened from the admin console. */ isAdminConsoleAction?: boolean; + + /** + * Function to restore a cipher from the trash. + */ + restore: (c: CipherView) => Promise; } export enum VaultItemDialogResult { @@ -96,6 +106,11 @@ export enum VaultItemDialogResult { * The dialog was closed to navigate the user the premium upgrade page. */ PremiumUpgrade = "premiumUpgrade", + + /** + * A cipher was restored + */ + Restored = "restored", } @Component({ @@ -112,11 +127,13 @@ export enum VaultItemDialogResult { CipherAttachmentsComponent, AsyncActionsModule, ItemModule, + DecryptionFailureDialogComponent, ], providers: [ { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, { provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService }, { provide: CipherFormGenerationService, useClass: WebCipherFormGenerationService }, + RoutedVaultFilterService, ], }) export class VaultItemDialogComponent implements OnInit, OnDestroy { @@ -181,7 +198,25 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { * Flag to indicate if the user has access to attachments via a premium subscription. * @protected */ - protected canAccessAttachments$ = this.billingAccountProfileStateService.hasPremiumFromAnySource$; + protected canAccessAttachments$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); + + /** + * Determines if the user may restore the item. + * A user may restore items if they have delete permissions and the item is in the trash. + */ + protected async canUserRestore() { + return ( + this.filter?.type === "trash" && + this.cipher?.isDeleted && + (await firstValueFrom(this.canDeleteCipher$)) + ); + } + + protected showRestore: boolean; protected get loadingForm() { return this.loadForm && !this.formReady; @@ -222,6 +257,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { protected canDeleteCipher$: Observable; + protected filter: RoutedVaultFilterModel; + constructor( @Inject(DIALOG_DATA) protected params: VaultItemDialogParams, private dialogRef: DialogRef, @@ -237,6 +274,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { private premiumUpgradeService: PremiumUpgradePromptService, private cipherAuthorizationService: CipherAuthorizationService, private apiService: ApiService, + private eventCollectionService: EventCollectionService, + private routedVaultFilterService: RoutedVaultFilterService, ) { this.updateTitle(); } @@ -245,6 +284,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.cipher = await this.getDecryptedCipherView(this.formConfig); if (this.cipher) { + if (this.cipher.decryptionFailure) { + this.dialogService.open(DecryptionFailureDialogComponent, { + data: { cipherIds: [this.cipher.id] }, + }); + this.dialogRef.close(); + return; + } + this.collections = this.formConfig.collections.filter((c) => this.cipher.collectionIds?.includes(c.id), ); @@ -257,8 +304,18 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { [this.params.activeCollectionId], this.params.isAdminConsoleAction, ); + + await this.eventCollectionService.collect( + EventType.Cipher_ClientViewed, + this.cipher.id, + false, + this.cipher.organizationId, + ); } + this.filter = await firstValueFrom(this.routedVaultFilterService.filter$); + + this.showRestore = await this.canUserRestore(); this.performingInitialLoad = false; } @@ -312,6 +369,11 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this._formReadySubject.next(); } + restore = async () => { + await this.params.restore(this.cipher); + this.dialogRef.close(VaultItemDialogResult.Restored); + }; + delete = async () => { if (!this.cipher) { return; @@ -419,19 +481,19 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { switch (type) { case CipherType.Login: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase()); + this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin")); break; case CipherType.Card: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase()); + this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard")); break; case CipherType.Identity: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase()); + this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); break; case CipherType.SecureNote: - this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase()); + this.title = this.i18nService.t(partOne, this.i18nService.t("note")); break; case CipherType.SshKey: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeSshKey").toLowerCase()); + this.title = this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); break; } } diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 5c4de576ead..7e59853851c 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -4,7 +4,7 @@ type="checkbox" bitCheckbox appStopProp - [disabled]="disabled" + [disabled]="disabled || cipher.decryptionFailure" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" [attr.aria-label]="'vaultItemSelect' | i18n" @@ -20,7 +20,7 @@ class="tw-overflow-hidden tw-text-ellipsis tw-text-start tw-leading-snug" [disabled]="disabled" [routerLink]="[]" - [queryParams]="{ itemId: cipher.id, action: extensionRefreshEnabled ? 'view' : null }" + [queryParams]="{ itemId: cipher.id, action: clickAction }" queryParamsHandling="merge" [replaceUrl]="extensionRefreshEnabled" title="{{ 'editItemWithName' | i18n: cipher.name }}" @@ -76,6 +76,25 @@ + + + + +
    + +
    + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index 80ed3025306..f2f16585742 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,26 +1,35 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; import { firstValueFrom, from, map } from "rxjs"; -import { switchMap, takeUntil } from "rxjs/operators"; +import { debounceTime, first, switchMap } from "rxjs/operators"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; import { PlanType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { WebProviderService } from "../services/web-provider.service"; import { AddOrganizationComponent } from "./add-organization.component"; -import { BaseClientsComponent } from "./base-clients.component"; const DisallowedPlanTypes = [ PlanType.Free, @@ -32,13 +41,26 @@ const DisallowedPlanTypes = [ @Component({ templateUrl: "clients.component.html", + standalone: true, + imports: [ + SharedOrganizationModule, + HeaderModule, + CommonModule, + JslibModule, + AvatarModule, + RouterModule, + TableModule, + ], }) -export class ClientsComponent extends BaseClientsComponent implements OnInit, OnDestroy { - providerId: string; - addableOrganizations: Organization[]; +export class ClientsComponent { + providerId: string = ""; + addableOrganizations: Organization[] = []; loading = true; manageOrganizations = false; showAddExisting = false; + dataSource: TableDataSource = + new TableDataSource(); + protected searchControl = new FormControl("", { nonNullable: true }); constructor( private router: Router, @@ -46,28 +68,19 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On private apiService: ApiService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - activatedRoute: ActivatedRoute, - dialogService: DialogService, - i18nService: I18nService, - searchService: SearchService, - toastService: ToastService, - validationService: ValidationService, - webProviderService: WebProviderService, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, ) { - super( - activatedRoute, - dialogService, - i18nService, - searchService, - toastService, - validationService, - webProviderService, - ); - } + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); - ngOnInit() { - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( @@ -85,18 +98,46 @@ export class ClientsComponent extends BaseClientsComponent implements OnInit, On }), ); }), - takeUntil(this.destroy$), + takeUntilDestroyed(), ) .subscribe(); + + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); } - ngOnDestroy() { - super.ngOnDestroy(); + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } } async load() { const response = await this.apiService.getProviderClients(this.providerId); - this.clients = response.data != null && response.data.length > 0 ? response.data : []; + const clients = response.data != null && response.data.length > 0 ? response.data : []; + this.dataSource.data = clients; this.manageOrganizations = (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; const candidateOrgs = (await this.organizationService.getAll()).filter( diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html deleted file mode 100644 index e6b5ae122f3..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - {{ "newClient" | i18n }} - - - - - - - {{ "loading" | i18n }} - - - -

    {{ "noClientsInList" | i18n }}

    - - - - {{ "name" | i18n }} - {{ "numberOfUsers" | i18n }} - {{ "billingPlan" | i18n }} - - - - - - - - {{ row.organizationName }} - - - {{ row.userCount }} - / {{ row.seats }} - - - {{ row.plan }} - - - - - - - -
    diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts deleted file mode 100644 index 2be38477d4c..00000000000 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/vnext-clients.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router, RouterModule } from "@angular/router"; -import { firstValueFrom, from, map } from "rxjs"; -import { debounceTime, first, switchMap } from "rxjs/operators"; - -import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { PlanType } from "@bitwarden/common/billing/enums"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - AvatarModule, - DialogService, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; - -import { WebProviderService } from "../services/web-provider.service"; - -import { AddOrganizationComponent } from "./add-organization.component"; - -const DisallowedPlanTypes = [ - PlanType.Free, - PlanType.FamiliesAnnually2019, - PlanType.FamiliesAnnually, - PlanType.TeamsStarter2023, - PlanType.TeamsStarter, -]; - -@Component({ - templateUrl: "vnext-clients.component.html", - standalone: true, - imports: [ - SharedOrganizationModule, - HeaderModule, - CommonModule, - JslibModule, - AvatarModule, - RouterModule, - TableModule, - ], -}) -export class vNextClientsComponent { - providerId: string = ""; - addableOrganizations: Organization[] = []; - loading = true; - manageOrganizations = false; - showAddExisting = false; - dataSource: TableDataSource = - new TableDataSource(); - protected searchControl = new FormControl("", { nonNullable: true }); - - constructor( - private router: Router, - private providerService: ProviderService, - private apiService: ApiService, - private organizationService: OrganizationService, - private organizationApiService: OrganizationApiServiceAbstraction, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private i18nService: I18nService, - private toastService: ToastService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); - - this.activatedRoute.parent?.params - ?.pipe( - switchMap((params) => { - this.providerId = params.providerId; - return this.providerService.get$(this.providerId).pipe( - map((provider) => provider?.providerStatus === ProviderStatusType.Billable), - map((isBillable) => { - if (isBillable) { - return from( - this.router.navigate(["../manage-client-organizations"], { - relativeTo: this.activatedRoute, - }), - ); - } else { - return from(this.load()); - } - }), - ); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((searchText) => { - this.dataSource.filter = (data) => - data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; - }); - } - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.webProviderService.detachOrganization(this.providerId, organization.id); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("detachedOrganization", organization.organizationName), - }); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - } - - async load() { - const response = await this.apiService.getProviderClients(this.providerId); - const clients = response.data != null && response.data.length > 0 ? response.data : []; - this.dataSource.data = clients; - this.manageOrganizations = - (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; - const candidateOrgs = (await this.organizationService.getAll()).filter( - (o) => o.isOwner && o.providerId == null, - ); - const allowedOrgsIds = await Promise.all( - candidateOrgs.map((o) => this.organizationApiService.get(o.id)), - ).then((orgs) => - orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id), - ); - this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id)); - - this.showAddExisting = this.addableOrganizations.length !== 0; - this.loading = false; - } - - async addExistingOrganization() { - const dialogRef = AddOrganizationComponent.open(this.dialogService, { - providerId: this.providerId, - organizations: this.addableOrganizations, - }); - - if (await firstValueFrom(dialogRef.closed)) { - await this.load(); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts index bd8c54c39d7..56584b67f02 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/accept-provider.component.ts @@ -2,9 +2,7 @@ // @ts-strict-ignore import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -33,9 +31,8 @@ export class AcceptProviderComponent extends BaseAcceptComponent { authService: AuthService, private apiService: ApiService, platformUtilService: PlatformUtilsService, - registerRouteService: RegisterRouteService, ) { - super(router, platformUtilService, i18nService, route, authService, registerRouteService); + super(router, platformUtilService, i18nService, route, authService); } async authedHandler(qParams: Params) { @@ -47,6 +44,7 @@ export class AcceptProviderComponent extends BaseAcceptComponent { qParams.providerUserId, request, ); + this.platformUtilService.showToast( "success", this.i18nService.t("inviteAccepted"), @@ -64,25 +62,14 @@ export class AcceptProviderComponent extends BaseAcceptComponent { } async register() { - let queryParams: Params; - let registerRoute = await firstValueFrom(this.registerRoute$); - if (registerRoute === "/register") { - queryParams = { - email: this.email, - }; - } else if (registerRoute === "/signup") { - // We have to override the base component route as we don't need users to - // complete email verification if they are coming directly an emailed invite. - registerRoute = "/finish-signup"; - queryParams = { + // We don't need users to complete email verification if they are coming directly from an emailed invite. + // Therefore, we skip /signup and navigate directly to /finish-signup. + await this.router.navigate(["/finish-signup"], { + queryParams: { email: this.email, providerUserId: this.providerUserId, providerInviteToken: this.providerInviteToken, - }; - } - - await this.router.navigate([registerRoute], { - queryParams: queryParams, + }, }); } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts index 09276263332..00c944e69bb 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-routing.module.ts @@ -2,10 +2,8 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { authGuard } from "@bitwarden/angular/auth/guards"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component"; import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component"; @@ -14,12 +12,10 @@ import { ProviderSubscriptionComponent, hasConsolidatedBilling, ProviderBillingHistoryComponent, - vNextManageClientsComponent, } from "../../billing/providers"; import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; -import { vNextClientsComponent } from "./clients/vnext-clients.component"; import { providerPermissionsGuard } from "./guards/provider-permissions.guard"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { EventsComponent } from "./manage/events.component"; @@ -86,25 +82,13 @@ const routes: Routes = [ children: [ { path: "", pathMatch: "full", redirectTo: "clients" }, { path: "clients/create", component: CreateOrganizationComponent }, - ...featureFlaggedRoute({ - defaultComponent: ClientsComponent, - flaggedComponent: vNextClientsComponent, - featureFlag: FeatureFlag.PM12443RemovePagingLogic, - routeOptions: { - path: "clients", - data: { titleId: "clients" }, - }, - }), - ...featureFlaggedRoute({ - defaultComponent: ManageClientsComponent, - flaggedComponent: vNextManageClientsComponent, - featureFlag: FeatureFlag.PM12443RemovePagingLogic, - routeOptions: { - path: "manage-client-organizations", - data: { titleId: "clients" }, - canActivate: [hasConsolidatedBilling], - }, - }), + { path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, + { + path: "manage-client-organizations", + canActivate: [hasConsolidatedBilling], + component: ManageClientsComponent, + data: { titleId: "clients" }, + }, { path: "manage", children: [ diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts index 80108e66eda..37cb9618b60 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers.module.ts @@ -11,9 +11,7 @@ import { OssModule } from "@bitwarden/web-vault/app/oss.module"; import { CreateClientDialogComponent, - NoClientsComponent, ManageClientNameDialogComponent, - ManageClientsComponent, ManageClientSubscriptionDialogComponent, ProviderBillingHistoryComponent, ProviderSubscriptionComponent, @@ -21,7 +19,6 @@ import { } from "../../billing/providers"; import { AddOrganizationComponent } from "./clients/add-organization.component"; -import { ClientsComponent } from "./clients/clients.component"; import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { AddEditMemberDialogComponent } from "./manage/dialogs/add-edit-member-dialog.component"; @@ -59,7 +56,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr AddOrganizationComponent, BulkConfirmDialogComponent, BulkRemoveDialogComponent, - ClientsComponent, CreateOrganizationComponent, EventsComponent, MembersComponent, @@ -68,8 +64,6 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr UserAddEditComponent, AddEditMemberDialogComponent, CreateClientDialogComponent, - NoClientsComponent, - ManageClientsComponent, ManageClientNameDialogComponent, ManageClientSubscriptionDialogComponent, ProviderBillingHistoryComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html index 33a20444c2b..74aa468c42e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.html @@ -29,7 +29,7 @@ - diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 46fd6989681..ecf649b8f31 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -23,8 +23,7 @@ import { KeyService } from "@bitwarden/key-management"; templateUrl: "setup.component.html", }) export class SetupComponent implements OnInit, OnDestroy { - @ViewChild(ManageTaxInformationComponent) - manageTaxInformationComponent: ManageTaxInformationComponent; + @ViewChild(ManageTaxInformationComponent) taxInformationComponent: ManageTaxInformationComponent; loading = true; providerId: string; @@ -111,9 +110,7 @@ export class SetupComponent implements OnInit, OnDestroy { try { this.formGroup.markAllAsTouched(); - const formIsValid = this.formGroup.valid && this.manageTaxInformationComponent.touch(); - - if (!formIsValid) { + if (!this.taxInformationComponent.validate() || !this.formGroup.valid) { return; } @@ -127,18 +124,15 @@ export class SetupComponent implements OnInit, OnDestroy { request.key = key; request.taxInfo = new ExpandedTaxInfoUpdateRequest(); - const taxInformation = this.manageTaxInformationComponent.getTaxInformation(); + const taxInformation = this.taxInformationComponent.getTaxInformation(); request.taxInfo.country = taxInformation.country; request.taxInfo.postalCode = taxInformation.postalCode; - - if (taxInformation.includeTaxId) { - request.taxInfo.taxId = taxInformation.taxId; - request.taxInfo.line1 = taxInformation.line1; - request.taxInfo.line2 = taxInformation.line2; - request.taxInfo.city = taxInformation.city; - request.taxInfo.state = taxInformation.state; - } + request.taxInfo.taxId = taxInformation.taxId; + request.taxInfo.line1 = taxInformation.line1; + request.taxInfo.line2 = taxInformation.line2; + request.taxInfo.city = taxInformation.city; + request.taxInfo.state = taxInformation.state; const provider = await this.providerApiService.postProviderSetup(this.providerId, request); @@ -152,6 +146,7 @@ export class SetupComponent implements OnInit, OnDestroy { await this.router.navigate(["/providers", provider.id]); } catch (e) { + e.message = this.i18nService.translate(e.message) || e.message; this.validationService.showError(e); } }; diff --git a/bitwarden_license/bit-web/src/app/app.module.ts b/bitwarden_license/bit-web/src/app/app.module.ts index fd1a3b0b84c..3a78ae0ed01 100644 --- a/bitwarden_license/bit-web/src/app/app.module.ts +++ b/bitwarden_license/bit-web/src/app/app.module.ts @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; -import { InfiniteScrollModule } from "ngx-infinite-scroll"; +import { InfiniteScrollDirective } from "ngx-infinite-scroll"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CoreModule } from "@bitwarden/web-vault/app/core"; @@ -37,7 +37,7 @@ import { AccessIntelligenceModule } from "./tools/access-intelligence/access-int FormsModule, ReactiveFormsModule, CoreModule, - InfiniteScrollModule, + InfiniteScrollDirective, DragDropModule, AppRoutingModule, OssRoutingModule, diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index 0731820e413..036163af3d9 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -89,19 +89,19 @@ {{ "trustedDevices" | i18n }} - {{ "memberDecryptionOptionTdeDescriptionPartOne" | i18n }} - {{ - "memberDecryptionOptionTdeDescriptionLinkOne" | i18n - }} - {{ "memberDecryptionOptionTdeDescriptionPartTwo" | i18n }} - {{ - "memberDecryptionOptionTdeDescriptionLinkTwo" | i18n - }} - {{ "memberDecryptionOptionTdeDescriptionPartThree" | i18n }} - {{ - "memberDecryptionOptionTdeDescriptionLinkThree" | i18n - }} - {{ "memberDecryptionOptionTdeDescriptionPartFour" | i18n }} + {{ "memberDecryptionOptionTdeDescPart1" | i18n }} + + {{ "memberDecryptionOptionTdeDescLink1" | i18n }} + + {{ "memberDecryptionOptionTdeDescPart2" | i18n }} + + {{ "memberDecryptionOptionTdeDescLink2" | i18n }} + + {{ "memberDecryptionOptionTdeDescPart3" | i18n }} + + {{ "memberDecryptionOptionTdeDescLink3" | i18n }} + + {{ "memberDecryptionOptionTdeDescPart4" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts index 05887fc198e..898d51e0baf 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/index.ts @@ -3,5 +3,4 @@ export * from "./manage-clients.component"; export * from "./manage-client-name-dialog.component"; export * from "./manage-client-subscription-dialog.component"; export * from "./no-clients.component"; -export * from "./vnext-manage-clients.component"; export * from "./replace.pipe"; diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html index 49a9208107f..7c560e49579 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.html @@ -1,5 +1,5 @@ - + {{ "addNewOrganization" | i18n }} @@ -16,78 +16,67 @@ - + - - {{ "client" | i18n }} - {{ "assigned" | i18n }} - {{ "used" | i18n }} - {{ "remaining" | i18n }} - {{ "billingPlan" | i18n }} - - + {{ "client" | i18n }} + {{ "assigned" | i18n }} + {{ "used" | i18n }} + {{ "remaining" | i18n }} + {{ "billingPlan" | i18n }} + - - - - - - - - - - {{ client.seats }} - - - {{ client.occupiedSeats }} - - - {{ client.remainingSeats }} - - - {{ removeMonthly(client.plan) }} - - - - - - - - - - + + + + + + + + + {{ row.seats }} + + + {{ row.occupiedSeats }} + + + {{ row.remainingSeats }} + + + {{ row.plan | replace: " (Monthly)" : "" }} + + + + + + + + + - -
    + +
    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts index d413551b743..ee2c541e72f 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-clients.component.ts @@ -1,23 +1,28 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { debounceTime, first, switchMap } from "rxjs/operators"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { DialogService, ToastService } from "@bitwarden/components"; +import { + AvatarModule, + DialogService, + TableDataSource, + TableModule, + ToastService, +} from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; -import { BaseClientsComponent } from "../../../admin-console/providers/clients/base-clients.component"; import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; import { @@ -32,47 +37,53 @@ import { ManageClientSubscriptionDialogResultType, openManageClientSubscriptionDialog, } from "./manage-client-subscription-dialog.component"; +import { NoClientsComponent } from "./no-clients.component"; +import { ReplacePipe } from "./replace.pipe"; @Component({ templateUrl: "manage-clients.component.html", + standalone: true, + imports: [ + AvatarModule, + TableModule, + HeaderModule, + SharedOrganizationModule, + NoClientsComponent, + ReplacePipe, + ], }) -export class ManageClientsComponent extends BaseClientsComponent { - providerId: string; - provider: Provider; - +export class ManageClientsComponent { + providerId: string = ""; + provider: Provider | undefined; loading = true; isProviderAdmin = false; + dataSource: TableDataSource = + new TableDataSource(); - protected plans: PlanResponse[]; + protected searchControl = new FormControl("", { nonNullable: true }); + protected plans: PlanResponse[] = []; constructor( - private billingApiService: BillingApiService, + private billingApiService: BillingApiServiceAbstraction, private providerService: ProviderService, private router: Router, - activatedRoute: ActivatedRoute, - dialogService: DialogService, - i18nService: I18nService, - searchService: SearchService, - toastService: ToastService, - validationService: ValidationService, - webProviderService: WebProviderService, + private activatedRoute: ActivatedRoute, + private dialogService: DialogService, + private i18nService: I18nService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, ) { - super( - activatedRoute, - dialogService, - i18nService, - searchService, - toastService, - validationService, - webProviderService, - ); + this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { + this.searchControl.setValue(queryParams.search); + }); - this.activatedRoute.parent.params - .pipe( + this.activatedRoute.parent?.params + ?.pipe( switchMap((params) => { this.providerId = params.providerId; return this.providerService.get$(this.providerId).pipe( - map((provider) => provider?.providerStatus === ProviderStatusType.Billable), + map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), map((isBillable) => { if (!isBillable) { return from( @@ -89,20 +100,24 @@ export class ManageClientsComponent extends BaseClientsComponent { takeUntilDestroyed(), ) .subscribe(); - } - removeMonthly = (plan: string) => plan.replace(" (Monthly)", ""); + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((searchText) => { + this.dataSource.filter = (data) => + data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + }); + } async load() { this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - this.isProviderAdmin = this.provider.type === ProviderUserType.ProviderAdmin; + this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; - this.clients = ( - await this.billingApiService.getProviderClientOrganizations(this.providerId) - ).data; + const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) + .data; - this.dataSource.data = this.clients; + this.dataSource.data = clients; this.plans = (await this.billingApiService.getPlans()).data; @@ -131,7 +146,7 @@ export class ManageClientsComponent extends BaseClientsComponent { organization: { id: organization.id, name: organization.organizationName, - seats: organization.seats, + seats: organization.seats ? organization.seats : 0, }, }, }); @@ -149,7 +164,7 @@ export class ManageClientsComponent extends BaseClientsComponent { const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { data: { organization, - provider: this.provider, + provider: this.provider!, }, }); @@ -159,4 +174,28 @@ export class ManageClientsComponent extends BaseClientsComponent { await this.load(); } }; + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } } diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts index 30ab444fe8b..7e4323d7603 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/no-clients.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; import { svgIcon } from "@bitwarden/components"; +import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; const gearIcon = svgIcon` @@ -23,6 +24,8 @@ const gearIcon = svgIcon` @Component({ selector: "app-no-clients", + standalone: true, + imports: [SharedOrganizationModule], template: `

    {{ "noClients" | i18n }}

    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html deleted file mode 100644 index 7c560e49579..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - {{ "addNewOrganization" | i18n }} - - - - - - {{ "loading" | i18n }} - - - - - - {{ "client" | i18n }} - {{ "assigned" | i18n }} - {{ "used" | i18n }} - {{ "remaining" | i18n }} - {{ "billingPlan" | i18n }} - - - - - - - - - - - {{ row.seats }} - - - {{ row.occupiedSeats }} - - - {{ row.remainingSeats }} - - - {{ row.plan | replace: " (Monthly)" : "" }} - - - - - - - - - - - -
    - -
    -
    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts deleted file mode 100644 index 94f615f0cee..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-manage-clients.component.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, from, lastValueFrom, map } from "rxjs"; -import { debounceTime, first, switchMap } from "rxjs/operators"; - -import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; -import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; -import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; -import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { - AvatarModule, - DialogService, - TableDataSource, - TableModule, - ToastService, -} from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; -import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; - -import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; - -import { - CreateClientDialogResultType, - openCreateClientDialog, -} from "./create-client-dialog.component"; -import { - ManageClientNameDialogResultType, - openManageClientNameDialog, -} from "./manage-client-name-dialog.component"; -import { - ManageClientSubscriptionDialogResultType, - openManageClientSubscriptionDialog, -} from "./manage-client-subscription-dialog.component"; -import { ReplacePipe } from "./replace.pipe"; -import { vNextNoClientsComponent } from "./vnext-no-clients.component"; - -@Component({ - templateUrl: "vnext-manage-clients.component.html", - standalone: true, - imports: [ - AvatarModule, - TableModule, - HeaderModule, - SharedOrganizationModule, - vNextNoClientsComponent, - ReplacePipe, - ], -}) -export class vNextManageClientsComponent { - providerId: string = ""; - provider: Provider | undefined; - loading = true; - isProviderAdmin = false; - dataSource: TableDataSource = - new TableDataSource(); - - protected searchControl = new FormControl("", { nonNullable: true }); - protected plans: PlanResponse[] = []; - - constructor( - private billingApiService: BillingApiServiceAbstraction, - private providerService: ProviderService, - private router: Router, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private i18nService: I18nService, - private toastService: ToastService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - ) { - this.activatedRoute.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((queryParams) => { - this.searchControl.setValue(queryParams.search); - }); - - this.activatedRoute.parent?.params - ?.pipe( - switchMap((params) => { - this.providerId = params.providerId; - return this.providerService.get$(this.providerId).pipe( - map((provider: Provider) => provider?.providerStatus === ProviderStatusType.Billable), - map((isBillable) => { - if (!isBillable) { - return from( - this.router.navigate(["../clients"], { - relativeTo: this.activatedRoute, - }), - ); - } else { - return from(this.load()); - } - }), - ); - }), - takeUntilDestroyed(), - ) - .subscribe(); - - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((searchText) => { - this.dataSource.filter = (data) => - data.organizationName.toLowerCase().indexOf(searchText.toLowerCase()) > -1; - }); - } - - async load() { - this.provider = await firstValueFrom(this.providerService.get$(this.providerId)); - - this.isProviderAdmin = this.provider?.type === ProviderUserType.ProviderAdmin; - - const clients = (await this.billingApiService.getProviderClientOrganizations(this.providerId)) - .data; - - this.dataSource.data = clients; - - this.plans = (await this.billingApiService.getPlans()).data; - - this.loading = false; - } - - createClient = async () => { - const reference = openCreateClientDialog(this.dialogService, { - data: { - providerId: this.providerId, - plans: this.plans, - }, - }); - - const result = await lastValueFrom(reference.closed); - - if (result === CreateClientDialogResultType.Submitted) { - await this.load(); - } - }; - - manageClientName = async (organization: ProviderOrganizationOrganizationDetailsResponse) => { - const dialogRef = openManageClientNameDialog(this.dialogService, { - data: { - providerId: this.providerId, - organization: { - id: organization.id, - name: organization.organizationName, - seats: organization.seats ? organization.seats : 0, - }, - }, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if (result === ManageClientNameDialogResultType.Submitted) { - await this.load(); - } - }; - - manageClientSubscription = async ( - organization: ProviderOrganizationOrganizationDetailsResponse, - ) => { - const dialogRef = openManageClientSubscriptionDialog(this.dialogService, { - data: { - organization, - provider: this.provider!, - }, - }); - - const result = await firstValueFrom(dialogRef.closed); - - if (result === ManageClientSubscriptionDialogResultType.Submitted) { - await this.load(); - } - }; - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return; - } - - try { - await this.webProviderService.detachOrganization(this.providerId, organization.id); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("detachedOrganization", organization.organizationName), - }); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - } -} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts deleted file mode 100644 index 5ad19945c51..00000000000 --- a/bitwarden_license/bit-web/src/app/billing/providers/clients/vnext-no-clients.component.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; - -import { svgIcon } from "@bitwarden/components"; -import { SharedOrganizationModule } from "@bitwarden/web-vault/app/admin-console/organizations/shared"; - -const gearIcon = svgIcon` - - - - - - - - - - - - - - - - -`; - -@Component({ - selector: "app-no-clients", - standalone: true, - imports: [SharedOrganizationModule], - template: `
    - -

    {{ "noClients" | i18n }}

    - - - {{ "addNewOrganization" | i18n }} - -
    `, -}) -export class vNextNoClientsComponent { - icon = gearIcon; - @Input() showAddOrganizationButton = true; - @Output() addNewOrganizationClicked = new EventEmitter(); - - addNewOrganization = () => this.addNewOrganizationClicked.emit(); -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts index e4a65f7ddd8..2d0a681460a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.spec.ts @@ -4,16 +4,15 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { SharedModule } from "@bitwarden/components/src/shared"; -import { - IntegrationCardComponent, - IntegrationGridComponent, -} from "@bitwarden/web-vault/app/shared"; +import {} from "@bitwarden/web-vault/app/shared"; -import { SYSTEM_THEME_OBSERVABLE } from "../../../../../../libs/angular/src/services/injection-tokens"; -import { I18nService } from "../../../../../../libs/common/src/platform/abstractions/i18n.service"; -import { ThemeType } from "../../../../../../libs/common/src/platform/enums"; -import { ThemeStateService } from "../../../../../../libs/common/src/platform/theming/theme-state.service"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component"; +import { IntegrationGridComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component"; import { IntegrationsComponent } from "./integrations.component"; @@ -35,7 +34,7 @@ describe("IntegrationsComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [IntegrationsComponent, MockHeaderComponent, MockNewMenuComponent], - imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule], + imports: [JslibModule, IntegrationGridComponent, IntegrationCardComponent], providers: [ { provide: I18nService, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts index af15c2c8b6d..cdae129de4f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.component.ts @@ -1,7 +1,7 @@ import { Component } from "@angular/core"; import { IntegrationType } from "@bitwarden/common/enums"; -import { Integration } from "@bitwarden/web-vault/app/shared"; +import { Integration } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/models"; @Component({ selector: "sm-integrations", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts index b79892f5ed6..eee426e3b07 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts @@ -1,9 +1,7 @@ import { NgModule } from "@angular/core"; -import { - IntegrationCardComponent, - IntegrationGridComponent, -} from "@bitwarden/web-vault/app/shared"; +import { IntegrationCardComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-card/integration-card.component"; +import { IntegrationGridComponent } from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/integrations/integration-grid/integration-grid.component"; import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts index b9c09a0d671..94d9ab2d8ee 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.module.ts @@ -1,8 +1,8 @@ import { NgModule } from "@angular/core"; import { BannerModule } from "@bitwarden/components"; +import { OnboardingModule } from "@bitwarden/web-vault/app/shared/components/onboarding/onboarding.module"; -import { OnboardingModule } from "../../../../../../apps/web/src/app/shared/components/onboarding/onboarding.module"; import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; import { OverviewRoutingModule } from "./overview-routing.module"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts index 631f6409748..78d367776d4 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts @@ -8,8 +8,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; +import { RouterService } from "@bitwarden/web-vault/app/core"; -import { RouterService } from "../../../../../../../apps/web/src/app/core/router.service"; import { ProjectView } from "../../models/view/project.view"; import { ProjectService } from "../project.service"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts index ebd00a83ea5..a4079a6def3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/guards/service-account-access.guard.spec.ts @@ -8,8 +8,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ToastService } from "@bitwarden/components"; +import { RouterService } from "@bitwarden/web-vault/app/core"; -import { RouterService } from "../../../../../../../../clients/apps/web/src/app/core/router.service"; import { ServiceAccountView } from "../../models/view/service-account.view"; import { ServiceAccountService } from "../service-account.service"; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts index 9ae9b2acec3..c5934067fd7 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/settings/services/sm-porting-api.service.ts @@ -107,6 +107,8 @@ export class SecretsManagerPortingApiService { return secret; }), ); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return null; } diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts index 2db7af4bb46..5f461ff6c49 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/access-intelligence.module.ts @@ -1,14 +1,19 @@ import { NgModule } from "@angular/core"; +import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; +import { CriticalAppsService } from "@bitwarden/bit-common/tools/reports/risk-insights"; import { + CriticalAppsApiService, MemberCipherDetailsApiService, RiskInsightsDataService, RiskInsightsReportService, } from "@bitwarden/bit-common/tools/reports/risk-insights/services"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength/password-strength.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { KeyService } from "@bitwarden/key-management"; import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module"; import { RiskInsightsComponent } from "./risk-insights.component"; @@ -33,6 +38,16 @@ import { RiskInsightsComponent } from "./risk-insights.component"; provide: RiskInsightsDataService, deps: [RiskInsightsReportService], }, + safeProvider({ + provide: CriticalAppsService, + useClass: CriticalAppsService, + deps: [KeyService, EncryptService, CriticalAppsApiService], + }), + safeProvider({ + provide: CriticalAppsApiService, + useClass: CriticalAppsApiService, + deps: [ApiService], + }), ], }) export class AccessIntelligenceModule {} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html index ea1a4f9db31..bcc15fbc8fc 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.html @@ -27,17 +27,19 @@

    {{ "allApplications" | i18n }}

    @@ -53,7 +55,7 @@ buttonType="secondary" bitButton *ngIf="isCriticalAppsFeatureEnabled" - [disabled]="!selectedIds.size" + [disabled]="!selectedUrls.size" [loading]="markingAsCritical" (click)="markAppsAsCritical()" > @@ -78,11 +80,13 @@ + - + {{ r.applicationName }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts index f4d3656071d..627495a6bde 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/all-applications.component.ts @@ -1,15 +1,17 @@ -import { Component, DestroyRef, OnDestroy, OnInit, inject } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { debounceTime, map, Observable, of, Subscription } from "rxjs"; +import { combineLatest, debounceTime, map, Observable, of, skipWhile } from "rxjs"; import { + CriticalAppsService, RiskInsightsDataService, RiskInsightsReportService, } from "@bitwarden/bit-common/tools/reports/risk-insights"; import { ApplicationHealthReportDetail, + ApplicationHealthReportDetailWithCriticalFlag, ApplicationHealthReportSummary, } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -19,6 +21,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { + DialogService, Icons, NoItemsModule, SearchModule, @@ -30,6 +33,9 @@ import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.mod import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; +import { openAppAtRiskMembersDialog } from "./app-at-risk-members-dialog.component"; +import { OrgAtRiskAppsDialogComponent } from "./org-at-risk-apps-dialog.component"; +import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component"; import { ApplicationsLoadingComponent } from "./risk-insights-loading.component"; @Component({ @@ -46,16 +52,20 @@ import { ApplicationsLoadingComponent } from "./risk-insights-loading.component" SharedModule, ], }) -export class AllApplicationsComponent implements OnInit, OnDestroy { - protected dataSource = new TableDataSource(); - protected selectedIds: Set = new Set(); +export class AllApplicationsComponent implements OnInit { + protected dataSource = new TableDataSource(); + protected selectedUrls: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); protected loading = true; - protected organization = {} as Organization; + protected organization = new Organization(); noItemsIcon = Icons.Security; protected markingAsCritical = false; - protected applicationSummary = {} as ApplicationHealthReportSummary; - private subscription = new Subscription(); + protected applicationSummary: ApplicationHealthReportSummary = { + totalMemberCount: 0, + totalAtRiskMemberCount: 0, + totalApplicationCount: 0, + totalAtRiskApplicationCount: 0, + }; destroyRef = inject(DestroyRef); isLoading$: Observable = of(false); @@ -66,28 +76,35 @@ export class AllApplicationsComponent implements OnInit, OnDestroy { FeatureFlag.CriticalApps, ); - const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId"); + const organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; + combineLatest([ + this.dataService.applications$, + this.criticalAppsService.getAppsListForOrg(organizationId), + this.organizationService.get$(organizationId), + ]) + .pipe( + takeUntilDestroyed(this.destroyRef), + skipWhile(([_, __, organization]) => !organization), + map(([applications, criticalApps, organization]) => { + const criticalUrls = criticalApps.map((ca) => ca.uri); + const data = applications?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as ApplicationHealthReportDetailWithCriticalFlag[]; + return { data, organization }; + }), + ) + .subscribe(({ data, organization }) => { + if (data) { + this.dataSource.data = data; + this.applicationSummary = this.reportService.generateApplicationsSummary(data); + } + if (organization) { + this.organization = organization; + } + }); - if (organizationId) { - this.organization = await this.organizationService.get(organizationId); - this.subscription = this.dataService.applications$ - .pipe( - map((applications) => { - if (applications) { - this.dataSource.data = applications; - this.applicationSummary = - this.reportService.generateApplicationsSummary(applications); - } - }), - takeUntilDestroyed(this.destroyRef), - ) - .subscribe(); - this.isLoading$ = this.dataService.isLoading$; - } - } - - ngOnDestroy(): void { - this.subscription?.unsubscribe(); + this.isLoading$ = this.dataService.isLoading$; } constructor( @@ -99,6 +116,8 @@ export class AllApplicationsComponent implements OnInit, OnDestroy { protected dataService: RiskInsightsDataService, protected organizationService: OrganizationService, protected reportService: RiskInsightsReportService, + protected criticalAppsService: CriticalAppsService, + protected dialogService: DialogService, ) { this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) @@ -114,33 +133,63 @@ export class AllApplicationsComponent implements OnInit, OnDestroy { }); }; + isMarkedAsCriticalItem(applicationName: string) { + return this.selectedUrls.has(applicationName); + } + markAppsAsCritical = async () => { - // TODO: Send to API once implemented this.markingAsCritical = true; - return new Promise((resolve) => { - setTimeout(() => { - this.selectedIds.clear(); - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("appsMarkedAsCritical"), - }); - resolve(true); - this.markingAsCritical = false; - }, 1000); - }); + + try { + await this.criticalAppsService.setCriticalApps( + this.organization.id, + Array.from(this.selectedUrls), + ); + + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("appsMarkedAsCritical"), + }); + } finally { + this.selectedUrls.clear(); + this.markingAsCritical = false; + } }; trackByFunction(_: number, item: ApplicationHealthReportDetail) { return item.applicationName; } - onCheckboxChange(id: number, event: Event) { + showAppAtRiskMembers = async (applicationName: string) => { + openAppAtRiskMembersDialog(this.dialogService, { + members: + this.dataSource.data.find((app) => app.applicationName === applicationName) + ?.atRiskMemberDetails ?? [], + applicationName, + }); + }; + + showOrgAtRiskMembers = async () => { + this.dialogService.open(OrgAtRiskMembersDialogComponent, { + data: this.reportService.generateAtRiskMemberList(this.dataSource.data), + }); + }; + + showOrgAtRiskApps = async () => { + this.dialogService.open(OrgAtRiskAppsDialogComponent, { + data: this.reportService.generateAtRiskApplicationList(this.dataSource.data), + }); + }; + + onCheckboxChange(applicationName: string, event: Event) { const isChecked = (event.target as HTMLInputElement).checked; if (isChecked) { - this.selectedIds.add(id); + this.selectedUrls.add(applicationName); } else { - this.selectedIds.delete(id); + this.selectedUrls.delete(applicationName); } } + + getSelectedUrls = () => Array.from(this.selectedUrls); } diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.html new file mode 100644 index 00000000000..fa58678be00 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.html @@ -0,0 +1,19 @@ + + {{ applicationName }} + +
    + {{ "atRiskMembersWithCount" | i18n: members.length }} + {{ "atRiskMembersDescriptionWithApp" | i18n: applicationName }} +
    + +
    {{ member.email }}
    +
    +
    +
    +
    + + + +
    diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts new file mode 100644 index 00000000000..d6a757fe897 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts @@ -0,0 +1,35 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { MemberDetailsFlat } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; + +type AppAtRiskMembersDialogParams = { + members: MemberDetailsFlat[]; + applicationName: string; +}; + +export const openAppAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AppAtRiskMembersDialogParams, +) => + dialogService.open(AppAtRiskMembersDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./app-at-risk-members-dialog.component.html", + imports: [ButtonModule, CommonModule, JslibModule, DialogModule], +}) +export class AppAtRiskMembersDialogComponent { + protected members: MemberDetailsFlat[]; + protected applicationName: string; + + constructor(@Inject(DIALOG_DATA) private params: AppAtRiskMembersDialogParams) { + this.members = params.members; + this.applicationName = params.applicationName; + } +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts deleted file mode 100644 index 4df363ab2c7..00000000000 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/application-table.mock.ts +++ /dev/null @@ -1,50 +0,0 @@ -export const applicationTableMockData = [ - { - id: 1, - name: "google.com", - atRiskPasswords: 4, - totalPasswords: 10, - atRiskMembers: 2, - totalMembers: 5, - }, - { - id: 2, - name: "facebook.com", - atRiskPasswords: 3, - totalPasswords: 8, - atRiskMembers: 1, - totalMembers: 3, - }, - { - id: 3, - name: "twitter.com", - atRiskPasswords: 2, - totalPasswords: 6, - atRiskMembers: 0, - totalMembers: 2, - }, - { - id: 4, - name: "linkedin.com", - atRiskPasswords: 1, - totalPasswords: 4, - atRiskMembers: 0, - totalMembers: 1, - }, - { - id: 5, - name: "instagram.com", - atRiskPasswords: 0, - totalPasswords: 2, - atRiskMembers: 0, - totalMembers: 0, - }, - { - id: 6, - name: "tiktok.com", - atRiskPasswords: 0, - totalPasswords: 1, - atRiskMembers: 0, - totalMembers: 0, - }, -]; diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html index 1c503f3d786..87b21c7c755 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html @@ -35,17 +35,19 @@
    @@ -60,38 +62,38 @@ - {{ "application" | i18n }} - {{ "atRiskPasswords" | i18n }} - {{ "totalPasswords" | i18n }} - {{ "atRiskMembers" | i18n }} - {{ "totalMembers" | i18n }} + {{ "application" | i18n }} + {{ "atRiskPasswords" | i18n }} + {{ "totalPasswords" | i18n }} + {{ "atRiskMembers" | i18n }} + {{ "totalMembers" | i18n }} - + - + - - {{ r.name }} + + {{ r.applicationName }} - {{ r.atRiskPasswords }} + {{ r.atRiskPasswordCount }} - {{ r.totalPasswords }} + {{ r.passwordCount }} - {{ r.atRiskMembers }} + {{ r.atRiskMemberCount }} - {{ r.totalMembers }} + {{ r.memberCount }} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts index 99f68aa9c72..450f0d5d660 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts @@ -4,16 +4,32 @@ import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { debounceTime, map } from "rxjs"; +import { combineLatest, debounceTime, map } from "rxjs"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components"; +import { + CriticalAppsService, + RiskInsightsDataService, + RiskInsightsReportService, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + ApplicationHealthReportDetailWithCriticalFlag, + ApplicationHealthReportSummary, +} from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { + DialogService, + Icons, + NoItemsModule, + SearchModule, + TableDataSource, +} from "@bitwarden/components"; import { CardComponent } from "@bitwarden/tools-card"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -import { applicationTableMockData } from "./application-table.mock"; +import { openAppAtRiskMembersDialog } from "./app-at-risk-members-dialog.component"; +import { OrgAtRiskAppsDialogComponent } from "./org-at-risk-apps-dialog.component"; +import { OrgAtRiskMembersDialogComponent } from "./org-at-risk-members-dialog.component"; import { RiskInsightsTabType } from "./risk-insights.component"; @Component({ @@ -23,30 +39,38 @@ import { RiskInsightsTabType } from "./risk-insights.component"; imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule], }) export class CriticalApplicationsComponent implements OnInit { - protected dataSource = new TableDataSource(); + protected dataSource = new TableDataSource(); protected selectedIds: Set = new Set(); protected searchControl = new FormControl("", { nonNullable: true }); private destroyRef = inject(DestroyRef); protected loading = false; protected organizationId: string; + protected applicationSummary = {} as ApplicationHealthReportSummary; noItemsIcon = Icons.Security; - // MOCK DATA - protected mockData = applicationTableMockData; - protected mockAtRiskMembersCount = 0; - protected mockAtRiskAppsCount = 0; - protected mockTotalMembersCount = 0; - protected mockTotalAppsCount = 0; ngOnInit() { - this.activatedRoute.paramMap + this.organizationId = this.activatedRoute.snapshot.paramMap.get("organizationId") ?? ""; + combineLatest([ + this.dataService.applications$, + this.criticalAppsService.getAppsListForOrg(this.organizationId), + ]) .pipe( takeUntilDestroyed(this.destroyRef), - map(async (params) => { - this.organizationId = params.get("organizationId"); - // TODO: use organizationId to fetch data + map(([applications, criticalApps]) => { + const criticalUrls = criticalApps.map((ca) => ca.uri); + const data = applications?.map((app) => ({ + ...app, + isMarkedAsCritical: criticalUrls.includes(app.applicationName), + })) as ApplicationHealthReportDetailWithCriticalFlag[]; + return data?.filter((app) => app.isMarkedAsCritical); }), ) - .subscribe(); + .subscribe((applications) => { + if (applications) { + this.dataSource.data = applications; + this.applicationSummary = this.reportService.generateApplicationsSummary(applications); + } + }); } goToAllAppsTab = async () => { @@ -57,13 +81,40 @@ export class CriticalApplicationsComponent implements OnInit { }; constructor( - protected i18nService: I18nService, protected activatedRoute: ActivatedRoute, protected router: Router, + protected dataService: RiskInsightsDataService, + protected criticalAppsService: CriticalAppsService, + protected reportService: RiskInsightsReportService, + protected dialogService: DialogService, ) { - this.dataSource.data = []; //applicationTableMockData; this.searchControl.valueChanges .pipe(debounceTime(200), takeUntilDestroyed()) .subscribe((v) => (this.dataSource.filter = v)); } + + showAppAtRiskMembers = async (applicationName: string) => { + openAppAtRiskMembersDialog(this.dialogService, { + members: + this.dataSource.data.find((app) => app.applicationName === applicationName) + ?.atRiskMemberDetails ?? [], + applicationName, + }); + }; + + showOrgAtRiskMembers = async () => { + this.dialogService.open(OrgAtRiskMembersDialogComponent, { + data: this.reportService.generateAtRiskMemberList(this.dataSource.data), + }); + }; + + showOrgAtRiskApps = async () => { + this.dialogService.open(OrgAtRiskAppsDialogComponent, { + data: this.reportService.generateAtRiskApplicationList(this.dataSource.data), + }); + }; + + trackByFunction(_: number, item: ApplicationHealthReportDetailWithCriticalFlag) { + return item.applicationName; + } } diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html new file mode 100644 index 00000000000..298011b2157 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html @@ -0,0 +1,25 @@ + + + {{ "atRiskApplicationsWithCount" | i18n: atRiskApps.length }} + + +
    + {{ "atRiskApplicationsDescription" | i18n }} +
    +
    {{ "application" | i18n }}
    +
    {{ "atRiskPasswords" | i18n }}
    +
    + +
    +
    {{ app.applicationName }}
    +
    {{ app.atRiskPasswordCount }}
    +
    +
    +
    +
    + + + +
    diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts new file mode 100644 index 00000000000..0ae00f60874 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts @@ -0,0 +1,24 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AtRiskApplicationDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; + +export const openOrgAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AtRiskApplicationDetail[], +) => + dialogService.open(OrgAtRiskAppsDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./org-at-risk-apps-dialog.component.html", + imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], +}) +export class OrgAtRiskAppsDialogComponent { + constructor(@Inject(DIALOG_DATA) protected atRiskApps: AtRiskApplicationDetail[]) {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html new file mode 100644 index 00000000000..1f1de103661 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html @@ -0,0 +1,25 @@ + + + {{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} + + +
    + {{ "atRiskMembersDescription" | i18n }} +
    +
    {{ "email" | i18n }}
    +
    {{ "atRiskPasswords" | i18n }}
    +
    + +
    +
    {{ member.email }}
    +
    {{ member.atRiskPasswordCount }}
    +
    +
    +
    +
    + + + +
    diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts new file mode 100644 index 00000000000..72518843d94 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts @@ -0,0 +1,24 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; + +export const openOrgAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AtRiskMemberDetail[], +) => + dialogService.open(OrgAtRiskMembersDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./org-at-risk-members-dialog.component.html", + imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], +}) +export class OrgAtRiskMembersDialogComponent { + constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index e0618c525a7..ae8bd94e5f3 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -3,7 +3,6 @@

    {{ "riskInsights" | i18n }}

    {{ "reviewAtRiskPasswords" | i18n }} -  {{ "learnMore" | i18n }}
    - {{ "criticalApplicationsWithCount" | i18n: criticalAppsCount }} + {{ "criticalApplicationsWithCount" | i18n: (criticalApps$ | async)?.length ?? 0 }} - + - + - + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 1a90e18f0df..5adb0d32945 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -6,10 +6,17 @@ import { Observable, EMPTY } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/risk-insights"; +import { + RiskInsightsDataService, + CriticalAppsService, + PasswordHealthReportApplicationsResponse, +} from "@bitwarden/bit-common/tools/reports/risk-insights"; import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +// eslint-disable-next-line no-restricted-imports -- used for dependency injection import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; @@ -50,6 +57,8 @@ export class RiskInsightsComponent implements OnInit { dataLastUpdated: Date = new Date(); isCriticalAppsFeatureEnabled: boolean = false; + criticalApps$: Observable = new Observable(); + showDebugTabs: boolean = false; appsCount: number = 0; criticalAppsCount: number = 0; @@ -67,10 +76,13 @@ export class RiskInsightsComponent implements OnInit { private router: Router, private configService: ConfigService, private dataService: RiskInsightsDataService, + private criticalAppsService: CriticalAppsService, ) { this.route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => { this.tabIndex = !isNaN(Number(tabIndex)) ? Number(tabIndex) : RiskInsightsTabType.AllApps; }); + const orgId = this.route.snapshot.paramMap.get("organizationId") ?? ""; + this.criticalApps$ = this.criticalAppsService.getAppsListForOrg(orgId); } async ngOnInit() { @@ -78,6 +90,8 @@ export class RiskInsightsComponent implements OnInit { FeatureFlag.CriticalApps, ); + this.showDebugTabs = devFlagEnabled("showRiskInsightsDebug"); + this.route.paramMap .pipe( takeUntilDestroyed(this.destroyRef), @@ -100,6 +114,7 @@ export class RiskInsightsComponent implements OnInit { if (applications) { this.appsCount = applications.length; } + this.criticalAppsService.setOrganizationId(this.organizationId as OrganizationId); }, }); } diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts index 6aaaf1a4066..321aae165c5 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts @@ -103,7 +103,7 @@ export class MemberAccessReportComponent implements OnInit { usesKeyConnector: user?.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: MemberDialogTab.Role, - numConfirmedMembers: this.dataSource.data.length, + numSeatsUsed: this.dataSource.data.length, }, }); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts index 9919da8d08e..cf2f3b6417b 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { OrganizationId } from "@bitwarden/common/src/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessExportItem } from "../view/member-access-export.view"; import { MemberAccessReportView } from "../view/member-access-report.view"; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts index 6aab54f77d5..7d6beca48ec 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { OrganizationId } from "@bitwarden/common/src/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; import { memberAccessReportsMock } from "./member-access-report.mock"; diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index 09de92d355d..13a6466b3b5 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -24,7 +24,9 @@ "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/key-management": ["../../libs/key-management/src"], + "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/platform": ["../../libs/platform/src"], + "@bitwarden/ui-common": ["../../libs/ui/common/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], "@bitwarden/vault": ["../../libs/vault/src"], diff --git a/clients.code-workspace b/clients.code-workspace index 1b956c25cee..f7d86d2a242 100644 --- a/clients.code-workspace +++ b/clients.code-workspace @@ -73,6 +73,15 @@ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "Angular.ng-template", + "nick-rudenko.back-n-forth", + "streetsidesoftware.code-spell-checker", + "MS-vsliveshare.vsliveshare", + "mhutchie.git-graph", + "donjayamanne.githistory", + "eamodio.gitlens", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "vadimcn.vscode-lldb", ], }, } diff --git a/libs/admin-console/jest.config.js b/libs/admin-console/jest.config.js index f2a8e6458af..5131753964c 100644 --- a/libs/admin-console/jest.config.js +++ b/libs/admin-console/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts index 4aa54429aad..048a4733948 100644 --- a/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts +++ b/libs/admin-console/src/common/collections/services/default-vnext-collection.service.spec.ts @@ -91,7 +91,7 @@ describe("DefaultvNextCollectionService", () => { // Assert emitted values expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: "DEC_NAME_" + collection1.id, @@ -167,7 +167,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(2); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("ENC_NAME_" + collection1.id), @@ -205,7 +205,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(3); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("UPDATED_ENC_NAME_" + collection1.id), @@ -230,7 +230,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(1); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: collection1.id, name: makeEncString("ENC_NAME_" + collection1.id), @@ -253,7 +253,7 @@ describe("DefaultvNextCollectionService", () => { const result = await firstValueFrom(collectionService.encryptedCollections$(userId)); expect(result.length).toBe(1); - expect(result).toIncludeAllPartialMembers([ + expect(result).toContainPartialObjects([ { id: newCollection3.id, name: makeEncString("ENC_NAME_" + newCollection3.id), diff --git a/libs/admin-console/test.setup.ts b/libs/admin-console/test.setup.ts index 6be6e7b8dd1..8ab102f2cf4 100644 --- a/libs/admin-console/test.setup.ts +++ b/libs/admin-console/test.setup.ts @@ -1,6 +1,10 @@ import { webcrypto } from "crypto"; + +import { addCustomMatchers } from "@bitwarden/common/spec"; import "jest-preset-angular/setup-jest"; +addCustomMatchers(); + Object.defineProperty(window, "CSS", { value: null }); Object.defineProperty(window, "getComputedStyle", { value: () => { diff --git a/libs/admin-console/tsconfig.json b/libs/admin-console/tsconfig.json index 6004a56fb55..4f057fd6af0 100644 --- a/libs/admin-console/tsconfig.json +++ b/libs/admin-console/tsconfig.json @@ -1,5 +1,13 @@ { - "extends": "../shared/tsconfig.libs", - "include": ["src", "spec"], + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/key-management": ["../key-management/src"] + } + }, + "include": ["src", "spec", "../../libs/common/custom-matchers.d.ts"], "exclude": ["node_modules", "dist"] } diff --git a/libs/angular/jest.config.js b/libs/angular/jest.config.js index c8e748575c0..5e73614eb8e 100644 --- a/libs/angular/jest.config.js +++ b/libs/angular/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/angular/src/auth/components/login-v1.component.ts b/libs/angular/src/auth/components/login-v1.component.ts index 0775114bd8c..3416901da97 100644 --- a/libs/angular/src/auth/components/login-v1.component.ts +++ b/libs/angular/src/auth/components/login-v1.component.ts @@ -10,11 +10,9 @@ import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, PasswordLoginCredentials, - RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -56,7 +54,7 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni return this.formGroup.controls.email; } - formGroup = this.formBuilder.group({ + formGroup = this.formBuilder.nonNullable.group({ email: ["", [Validators.required, Validators.email]], masterPassword: [ "", @@ -67,14 +65,12 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni protected twoFactorRoute = "2fa"; protected successRoute = "vault"; - // TODO: remove when email verification flag is removed - protected registerRoute$ = this.registerRouteService.registerRoute$(); protected forcePasswordResetRoute = "update-temp-password"; protected destroy$ = new Subject(); get loggedEmail() { - return this.formGroup.value.email; + return this.formGroup.controls.email.value; } constructor( @@ -95,8 +91,6 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni protected route: ActivatedRoute, protected loginEmailService: LoginEmailServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, - protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, - protected registerRouteService: RegisterRouteService, protected toastService: ToastService, ) { super(environmentService, i18nService, platformUtilsService, toastService); @@ -146,8 +140,6 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni } async submit(showToast = true) { - const data = this.formGroup.value; - await this.setupCaptcha(); this.formGroup.markAllAsTouched(); @@ -170,10 +162,10 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni try { const credentials = new PasswordLoginCredentials( - data.email, - data.masterPassword, + this.formGroup.controls.email.value, + this.formGroup.controls.masterPassword.value, this.captchaToken, - null, + undefined, ); this.formPromise = this.loginStrategyService.logIn(credentials); @@ -397,6 +389,8 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni email, deviceIdentifier, ); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.showLoginWithDevice = false; } diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 8b4df78175f..6c13809566a 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -19,7 +19,6 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -50,7 +49,7 @@ export class SsoComponent implements OnInit { protected twoFactorRoute = "2fa"; protected successRoute = "lock"; protected trustedDeviceEncRoute = "login-initiated"; - protected changePasswordRoute = "set-password"; + protected changePasswordRoute = "set-password-jit"; protected forcePasswordResetRoute = "update-temp-password"; protected clientId: string; protected redirectUri: string; @@ -339,14 +338,6 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - if (emailVerification) { - this.changePasswordRoute = "set-password-jit"; - } - await this.navigateViaCallbackOrRoute( this.onSuccessfulLoginChangePasswordNavigate, [this.changePasswordRoute], diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 18bfe546600..e2b41ad086d 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -157,7 +157,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), - message: error, + message: this.i18nService.t("webauthnCancelOrTimeout"), }); }, (info: string) => { diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html index 0b041bd4c06..3f635656fb7 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.html @@ -1,7 +1,7 @@
    - + {{ "country" | i18n }}
    - + {{ "zipPostalCode" | i18n }}
    -
    - - - {{ "includeVAT" | i18n }} - + +
    + + {{ "address1" | i18n }} + + +
    +
    + + {{ "address2" | i18n }} + + +
    +
    + + {{ "cityTown" | i18n }} + + +
    +
    + + {{ "stateProvince" | i18n }} + + +
    +
    + + {{ "taxIdNumber" | i18n }} + + +
    +
    +
    +
    -
    -
    - - {{ "taxIdNumber" | i18n }} - - -
    -
    -
    -
    - - {{ "address1" | i18n }} - - -
    -
    - - {{ "address2" | i18n }} - - -
    -
    - - {{ "cityTown" | i18n }} - - -
    -
    - - {{ "stateProvince" | i18n }} - - -
    -
    - diff --git a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts index a3564b1ebc9..885afb1ae67 100644 --- a/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts +++ b/libs/angular/src/billing/components/manage-tax-information/manage-tax-information.component.ts @@ -3,14 +3,10 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; +import { debounceTime } from "rxjs/operators"; -import { TaxInformation } from "@bitwarden/common/billing/models/domain"; - -type Country = { - name: string; - value: string; - disabled: boolean; -}; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; +import { CountryListItem, TaxInformation } from "@bitwarden/common/billing/models/domain"; @Component({ selector: "app-manage-tax-information", @@ -19,12 +15,23 @@ type Country = { export class ManageTaxInformationComponent implements OnInit, OnDestroy { @Input() startWith: TaxInformation; @Input() onSubmit?: (taxInformation: TaxInformation) => Promise; + @Input() showTaxIdField: boolean = true; + + /** + * Emits when the tax information has changed. + */ + @Output() taxInformationChanged = new EventEmitter(); + + /** + * Emits when the tax information has been updated. + */ @Output() taxInformationUpdated = new EventEmitter(); + private taxInformation: TaxInformation; + protected formGroup = this.formBuilder.group({ country: ["", Validators.required], postalCode: ["", Validators.required], - includeTaxId: false, taxId: "", line1: "", line2: "", @@ -32,16 +39,20 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { state: "", }); + protected isTaxSupported: boolean; + private destroy$ = new Subject(); - private taxInformation: TaxInformation; + protected readonly countries: CountryListItem[] = this.taxService.getCountries(); - constructor(private formBuilder: FormBuilder) {} + constructor( + private formBuilder: FormBuilder, + private taxService: TaxServiceAbstraction, + ) {} - getTaxInformation = (): TaxInformation & { includeTaxId: boolean } => ({ - ...this.taxInformation, - includeTaxId: this.formGroup.value.includeTaxId, - }); + getTaxInformation(): TaxInformation { + return this.taxInformation; + } submit = async () => { this.formGroup.markAllAsTouched(); @@ -52,23 +63,36 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { this.taxInformationUpdated.emit(); }; - touch = (): boolean => { + validate(): boolean { + if (this.formGroup.dirty) { + this.formGroup.markAllAsTouched(); + return this.formGroup.valid; + } else { + return this.formGroup.valid; + } + } + + markAllAsTouched() { this.formGroup.markAllAsTouched(); - return this.formGroup.valid; - }; + } async ngOnInit() { if (this.startWith) { - this.formGroup.patchValue({ - ...this.startWith, - includeTaxId: - this.countrySupportsTax(this.startWith.country) && - (!!this.startWith.taxId || - !!this.startWith.line1 || - !!this.startWith.line2 || - !!this.startWith.city || - !!this.startWith.state), - }); + this.formGroup.controls.country.setValue(this.startWith.country); + this.formGroup.controls.postalCode.setValue(this.startWith.postalCode); + + this.isTaxSupported = + this.startWith && this.startWith.country + ? await this.taxService.isCountrySupported(this.startWith.country) + : false; + + if (this.isTaxSupported) { + this.formGroup.controls.taxId.setValue(this.startWith.taxId); + this.formGroup.controls.line1.setValue(this.startWith.line1); + this.formGroup.controls.line2.setValue(this.startWith.line2); + this.formGroup.controls.city.setValue(this.startWith.city); + this.formGroup.controls.state.setValue(this.startWith.state); + } } this.formGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((values) => { @@ -82,354 +106,47 @@ export class ManageTaxInformationComponent implements OnInit, OnDestroy { state: values.state, }; }); + + this.formGroup.controls.country.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe((country: string) => { + this.taxService + .isCountrySupported(country) + .then((isSupported) => (this.isTaxSupported = isSupported)) + .catch(() => (this.isTaxSupported = false)) + .finally(() => { + if (!this.isTaxSupported) { + this.formGroup.controls.taxId.setValue(null); + this.formGroup.controls.line1.setValue(null); + this.formGroup.controls.line2.setValue(null); + this.formGroup.controls.city.setValue(null); + this.formGroup.controls.state.setValue(null); + } + if (this.taxInformationChanged) { + this.taxInformationChanged.emit(this.taxInformation); + } + }); + }); + + this.formGroup.controls.postalCode.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + if (this.taxInformationChanged) { + this.taxInformationChanged.emit(this.taxInformation); + } + }); + + this.formGroup.controls.taxId.valueChanges + .pipe(debounceTime(1000), takeUntil(this.destroy$)) + .subscribe(() => { + if (this.taxInformationChanged) { + this.taxInformationChanged.emit(this.taxInformation); + } + }); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } - - protected countrySupportsTax(countryCode: string) { - return this.taxSupportedCountryCodes.includes(countryCode); - } - - protected get includeTaxIdIsSelected() { - return this.formGroup.value.includeTaxId; - } - - protected get selectionSupportsAdditionalOptions() { - return ( - this.formGroup.value.country !== "US" && this.countrySupportsTax(this.formGroup.value.country) - ); - } - - protected countries: Country[] = [ - { name: "-- Select --", value: "", disabled: false }, - { name: "United States", value: "US", disabled: false }, - { name: "China", value: "CN", disabled: false }, - { name: "France", value: "FR", disabled: false }, - { name: "Germany", value: "DE", disabled: false }, - { name: "Canada", value: "CA", disabled: false }, - { name: "United Kingdom", value: "GB", disabled: false }, - { name: "Australia", value: "AU", disabled: false }, - { name: "India", value: "IN", disabled: false }, - { name: "", value: "-", disabled: true }, - { name: "Afghanistan", value: "AF", disabled: false }, - { name: "Åland Islands", value: "AX", disabled: false }, - { name: "Albania", value: "AL", disabled: false }, - { name: "Algeria", value: "DZ", disabled: false }, - { name: "American Samoa", value: "AS", disabled: false }, - { name: "Andorra", value: "AD", disabled: false }, - { name: "Angola", value: "AO", disabled: false }, - { name: "Anguilla", value: "AI", disabled: false }, - { name: "Antarctica", value: "AQ", disabled: false }, - { name: "Antigua and Barbuda", value: "AG", disabled: false }, - { name: "Argentina", value: "AR", disabled: false }, - { name: "Armenia", value: "AM", disabled: false }, - { name: "Aruba", value: "AW", disabled: false }, - { name: "Austria", value: "AT", disabled: false }, - { name: "Azerbaijan", value: "AZ", disabled: false }, - { name: "Bahamas", value: "BS", disabled: false }, - { name: "Bahrain", value: "BH", disabled: false }, - { name: "Bangladesh", value: "BD", disabled: false }, - { name: "Barbados", value: "BB", disabled: false }, - { name: "Belarus", value: "BY", disabled: false }, - { name: "Belgium", value: "BE", disabled: false }, - { name: "Belize", value: "BZ", disabled: false }, - { name: "Benin", value: "BJ", disabled: false }, - { name: "Bermuda", value: "BM", disabled: false }, - { name: "Bhutan", value: "BT", disabled: false }, - { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, - { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, - { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, - { name: "Botswana", value: "BW", disabled: false }, - { name: "Bouvet Island", value: "BV", disabled: false }, - { name: "Brazil", value: "BR", disabled: false }, - { name: "British Indian Ocean Territory", value: "IO", disabled: false }, - { name: "Brunei Darussalam", value: "BN", disabled: false }, - { name: "Bulgaria", value: "BG", disabled: false }, - { name: "Burkina Faso", value: "BF", disabled: false }, - { name: "Burundi", value: "BI", disabled: false }, - { name: "Cambodia", value: "KH", disabled: false }, - { name: "Cameroon", value: "CM", disabled: false }, - { name: "Cape Verde", value: "CV", disabled: false }, - { name: "Cayman Islands", value: "KY", disabled: false }, - { name: "Central African Republic", value: "CF", disabled: false }, - { name: "Chad", value: "TD", disabled: false }, - { name: "Chile", value: "CL", disabled: false }, - { name: "Christmas Island", value: "CX", disabled: false }, - { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, - { name: "Colombia", value: "CO", disabled: false }, - { name: "Comoros", value: "KM", disabled: false }, - { name: "Congo", value: "CG", disabled: false }, - { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, - { name: "Cook Islands", value: "CK", disabled: false }, - { name: "Costa Rica", value: "CR", disabled: false }, - { name: "Côte d'Ivoire", value: "CI", disabled: false }, - { name: "Croatia", value: "HR", disabled: false }, - { name: "Cuba", value: "CU", disabled: false }, - { name: "Curaçao", value: "CW", disabled: false }, - { name: "Cyprus", value: "CY", disabled: false }, - { name: "Czech Republic", value: "CZ", disabled: false }, - { name: "Denmark", value: "DK", disabled: false }, - { name: "Djibouti", value: "DJ", disabled: false }, - { name: "Dominica", value: "DM", disabled: false }, - { name: "Dominican Republic", value: "DO", disabled: false }, - { name: "Ecuador", value: "EC", disabled: false }, - { name: "Egypt", value: "EG", disabled: false }, - { name: "El Salvador", value: "SV", disabled: false }, - { name: "Equatorial Guinea", value: "GQ", disabled: false }, - { name: "Eritrea", value: "ER", disabled: false }, - { name: "Estonia", value: "EE", disabled: false }, - { name: "Ethiopia", value: "ET", disabled: false }, - { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, - { name: "Faroe Islands", value: "FO", disabled: false }, - { name: "Fiji", value: "FJ", disabled: false }, - { name: "Finland", value: "FI", disabled: false }, - { name: "French Guiana", value: "GF", disabled: false }, - { name: "French Polynesia", value: "PF", disabled: false }, - { name: "French Southern Territories", value: "TF", disabled: false }, - { name: "Gabon", value: "GA", disabled: false }, - { name: "Gambia", value: "GM", disabled: false }, - { name: "Georgia", value: "GE", disabled: false }, - { name: "Ghana", value: "GH", disabled: false }, - { name: "Gibraltar", value: "GI", disabled: false }, - { name: "Greece", value: "GR", disabled: false }, - { name: "Greenland", value: "GL", disabled: false }, - { name: "Grenada", value: "GD", disabled: false }, - { name: "Guadeloupe", value: "GP", disabled: false }, - { name: "Guam", value: "GU", disabled: false }, - { name: "Guatemala", value: "GT", disabled: false }, - { name: "Guernsey", value: "GG", disabled: false }, - { name: "Guinea", value: "GN", disabled: false }, - { name: "Guinea-Bissau", value: "GW", disabled: false }, - { name: "Guyana", value: "GY", disabled: false }, - { name: "Haiti", value: "HT", disabled: false }, - { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, - { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, - { name: "Honduras", value: "HN", disabled: false }, - { name: "Hong Kong", value: "HK", disabled: false }, - { name: "Hungary", value: "HU", disabled: false }, - { name: "Iceland", value: "IS", disabled: false }, - { name: "Indonesia", value: "ID", disabled: false }, - { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, - { name: "Iraq", value: "IQ", disabled: false }, - { name: "Ireland", value: "IE", disabled: false }, - { name: "Isle of Man", value: "IM", disabled: false }, - { name: "Israel", value: "IL", disabled: false }, - { name: "Italy", value: "IT", disabled: false }, - { name: "Jamaica", value: "JM", disabled: false }, - { name: "Japan", value: "JP", disabled: false }, - { name: "Jersey", value: "JE", disabled: false }, - { name: "Jordan", value: "JO", disabled: false }, - { name: "Kazakhstan", value: "KZ", disabled: false }, - { name: "Kenya", value: "KE", disabled: false }, - { name: "Kiribati", value: "KI", disabled: false }, - { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, - { name: "Korea, Republic of", value: "KR", disabled: false }, - { name: "Kuwait", value: "KW", disabled: false }, - { name: "Kyrgyzstan", value: "KG", disabled: false }, - { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, - { name: "Latvia", value: "LV", disabled: false }, - { name: "Lebanon", value: "LB", disabled: false }, - { name: "Lesotho", value: "LS", disabled: false }, - { name: "Liberia", value: "LR", disabled: false }, - { name: "Libya", value: "LY", disabled: false }, - { name: "Liechtenstein", value: "LI", disabled: false }, - { name: "Lithuania", value: "LT", disabled: false }, - { name: "Luxembourg", value: "LU", disabled: false }, - { name: "Macao", value: "MO", disabled: false }, - { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, - { name: "Madagascar", value: "MG", disabled: false }, - { name: "Malawi", value: "MW", disabled: false }, - { name: "Malaysia", value: "MY", disabled: false }, - { name: "Maldives", value: "MV", disabled: false }, - { name: "Mali", value: "ML", disabled: false }, - { name: "Malta", value: "MT", disabled: false }, - { name: "Marshall Islands", value: "MH", disabled: false }, - { name: "Martinique", value: "MQ", disabled: false }, - { name: "Mauritania", value: "MR", disabled: false }, - { name: "Mauritius", value: "MU", disabled: false }, - { name: "Mayotte", value: "YT", disabled: false }, - { name: "Mexico", value: "MX", disabled: false }, - { name: "Micronesia, Federated States of", value: "FM", disabled: false }, - { name: "Moldova, Republic of", value: "MD", disabled: false }, - { name: "Monaco", value: "MC", disabled: false }, - { name: "Mongolia", value: "MN", disabled: false }, - { name: "Montenegro", value: "ME", disabled: false }, - { name: "Montserrat", value: "MS", disabled: false }, - { name: "Morocco", value: "MA", disabled: false }, - { name: "Mozambique", value: "MZ", disabled: false }, - { name: "Myanmar", value: "MM", disabled: false }, - { name: "Namibia", value: "NA", disabled: false }, - { name: "Nauru", value: "NR", disabled: false }, - { name: "Nepal", value: "NP", disabled: false }, - { name: "Netherlands", value: "NL", disabled: false }, - { name: "New Caledonia", value: "NC", disabled: false }, - { name: "New Zealand", value: "NZ", disabled: false }, - { name: "Nicaragua", value: "NI", disabled: false }, - { name: "Niger", value: "NE", disabled: false }, - { name: "Nigeria", value: "NG", disabled: false }, - { name: "Niue", value: "NU", disabled: false }, - { name: "Norfolk Island", value: "NF", disabled: false }, - { name: "Northern Mariana Islands", value: "MP", disabled: false }, - { name: "Norway", value: "NO", disabled: false }, - { name: "Oman", value: "OM", disabled: false }, - { name: "Pakistan", value: "PK", disabled: false }, - { name: "Palau", value: "PW", disabled: false }, - { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, - { name: "Panama", value: "PA", disabled: false }, - { name: "Papua New Guinea", value: "PG", disabled: false }, - { name: "Paraguay", value: "PY", disabled: false }, - { name: "Peru", value: "PE", disabled: false }, - { name: "Philippines", value: "PH", disabled: false }, - { name: "Pitcairn", value: "PN", disabled: false }, - { name: "Poland", value: "PL", disabled: false }, - { name: "Portugal", value: "PT", disabled: false }, - { name: "Puerto Rico", value: "PR", disabled: false }, - { name: "Qatar", value: "QA", disabled: false }, - { name: "Réunion", value: "RE", disabled: false }, - { name: "Romania", value: "RO", disabled: false }, - { name: "Russian Federation", value: "RU", disabled: false }, - { name: "Rwanda", value: "RW", disabled: false }, - { name: "Saint Barthélemy", value: "BL", disabled: false }, - { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, - { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, - { name: "Saint Lucia", value: "LC", disabled: false }, - { name: "Saint Martin (French part)", value: "MF", disabled: false }, - { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, - { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, - { name: "Samoa", value: "WS", disabled: false }, - { name: "San Marino", value: "SM", disabled: false }, - { name: "Sao Tome and Principe", value: "ST", disabled: false }, - { name: "Saudi Arabia", value: "SA", disabled: false }, - { name: "Senegal", value: "SN", disabled: false }, - { name: "Serbia", value: "RS", disabled: false }, - { name: "Seychelles", value: "SC", disabled: false }, - { name: "Sierra Leone", value: "SL", disabled: false }, - { name: "Singapore", value: "SG", disabled: false }, - { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, - { name: "Slovakia", value: "SK", disabled: false }, - { name: "Slovenia", value: "SI", disabled: false }, - { name: "Solomon Islands", value: "SB", disabled: false }, - { name: "Somalia", value: "SO", disabled: false }, - { name: "South Africa", value: "ZA", disabled: false }, - { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, - { name: "South Sudan", value: "SS", disabled: false }, - { name: "Spain", value: "ES", disabled: false }, - { name: "Sri Lanka", value: "LK", disabled: false }, - { name: "Sudan", value: "SD", disabled: false }, - { name: "Suriname", value: "SR", disabled: false }, - { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, - { name: "Swaziland", value: "SZ", disabled: false }, - { name: "Sweden", value: "SE", disabled: false }, - { name: "Switzerland", value: "CH", disabled: false }, - { name: "Syrian Arab Republic", value: "SY", disabled: false }, - { name: "Taiwan", value: "TW", disabled: false }, - { name: "Tajikistan", value: "TJ", disabled: false }, - { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, - { name: "Thailand", value: "TH", disabled: false }, - { name: "Timor-Leste", value: "TL", disabled: false }, - { name: "Togo", value: "TG", disabled: false }, - { name: "Tokelau", value: "TK", disabled: false }, - { name: "Tonga", value: "TO", disabled: false }, - { name: "Trinidad and Tobago", value: "TT", disabled: false }, - { name: "Tunisia", value: "TN", disabled: false }, - { name: "Turkey", value: "TR", disabled: false }, - { name: "Turkmenistan", value: "TM", disabled: false }, - { name: "Turks and Caicos Islands", value: "TC", disabled: false }, - { name: "Tuvalu", value: "TV", disabled: false }, - { name: "Uganda", value: "UG", disabled: false }, - { name: "Ukraine", value: "UA", disabled: false }, - { name: "United Arab Emirates", value: "AE", disabled: false }, - { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, - { name: "Uruguay", value: "UY", disabled: false }, - { name: "Uzbekistan", value: "UZ", disabled: false }, - { name: "Vanuatu", value: "VU", disabled: false }, - { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, - { name: "Viet Nam", value: "VN", disabled: false }, - { name: "Virgin Islands, British", value: "VG", disabled: false }, - { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, - { name: "Wallis and Futuna", value: "WF", disabled: false }, - { name: "Western Sahara", value: "EH", disabled: false }, - { name: "Yemen", value: "YE", disabled: false }, - { name: "Zambia", value: "ZM", disabled: false }, - { name: "Zimbabwe", value: "ZW", disabled: false }, - ]; - - private taxSupportedCountryCodes: string[] = [ - "CN", - "FR", - "DE", - "CA", - "GB", - "AU", - "IN", - "AD", - "AR", - "AT", - "BE", - "BO", - "BR", - "BG", - "CL", - "CO", - "CR", - "HR", - "CY", - "CZ", - "DK", - "DO", - "EC", - "EG", - "SV", - "EE", - "FI", - "GE", - "GR", - "HK", - "HU", - "IS", - "ID", - "IQ", - "IE", - "IL", - "IT", - "JP", - "KE", - "KR", - "LV", - "LI", - "LT", - "LU", - "MY", - "MT", - "MX", - "NL", - "NZ", - "NO", - "PE", - "PH", - "PL", - "PT", - "RO", - "RU", - "SA", - "RS", - "SG", - "SK", - "SI", - "ZA", - "ES", - "SE", - "CH", - "TW", - "TH", - "TR", - "UA", - "AE", - "UY", - "VE", - "VN", - ]; } diff --git a/libs/angular/src/directives/not-premium.directive.ts b/libs/angular/src/directives/not-premium.directive.ts index 3aee9b192d2..5a1c636c009 100644 --- a/libs/angular/src/directives/not-premium.directive.ts +++ b/libs/angular/src/directives/not-premium.directive.ts @@ -1,6 +1,7 @@ import { Directive, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; /** @@ -14,11 +15,19 @@ export class NotPremiumDirective implements OnInit { private templateRef: TemplateRef, private viewContainer: ViewContainerRef, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} async ngOnInit(): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); + + if (!account) { + this.viewContainer.createEmbeddedView(this.templateRef); + return; + } + const premium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); if (premium) { diff --git a/libs/angular/src/directives/premium.directive.ts b/libs/angular/src/directives/premium.directive.ts index d475669a1ab..2188205ba65 100644 --- a/libs/angular/src/directives/premium.directive.ts +++ b/libs/angular/src/directives/premium.directive.ts @@ -1,6 +1,7 @@ import { Directive, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; +import { of, Subject, switchMap, takeUntil } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; /** @@ -16,16 +17,24 @@ export class PremiumDirective implements OnInit, OnDestroy { private templateRef: TemplateRef, private viewContainer: ViewContainerRef, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} async ngOnInit(): Promise { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.directiveIsDestroyed$)) + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + account + ? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), + takeUntil(this.directiveIsDestroyed$), + ) .subscribe((premium: boolean) => { if (premium) { - this.viewContainer.clear(); - } else { this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); } }); } diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 4f5a8f6673c..b06faacef19 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -25,15 +25,15 @@ import { TableModule, ToastModule, TypographyModule, + CopyClickDirective, + A11yTitleDirective, } from "@bitwarden/components"; import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component"; import { DeprecatedCalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; -import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; import { BoxRowDirective } from "./directives/box-row.directive"; -import { CopyClickDirective } from "./directives/copy-click.directive"; import { CopyTextDirective } from "./directives/copy-text.directive"; import { FallbackSrcDirective } from "./directives/fallback-src.directive"; import { IfFeatureDirective } from "./directives/if-feature.directive"; @@ -83,10 +83,11 @@ import { IconComponent } from "./vault/components/icon.component"; LinkModule, IconModule, TextDragDirective, + CopyClickDirective, + A11yTitleDirective, ], declarations: [ A11yInvalidDirective, - A11yTitleDirective, ApiActionDirective, AutofocusDirective, BoxRowDirective, @@ -105,7 +106,6 @@ import { IconComponent } from "./vault/components/icon.component"; StopClickDirective, StopPropDirective, TrueFalseValueDirective, - CopyClickDirective, LaunchClickDirective, UserNamePipe, PasswordStrengthComponent, diff --git a/libs/angular/src/platform/pipes/i18n.pipe.ts b/libs/angular/src/platform/pipes/i18n.pipe.ts index 1f92bbb19a4..a6fdbc78255 100644 --- a/libs/angular/src/platform/pipes/i18n.pipe.ts +++ b/libs/angular/src/platform/pipes/i18n.pipe.ts @@ -2,6 +2,9 @@ import { Pipe, PipeTransform } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +/** + * @deprecated: Please use the I18nPipe from @bitwarden/ui-common + */ @Pipe({ name: "i18n", }) diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index e7547f9b828..82b5affcc22 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -1,138 +1,4 @@ -import { Provider } from "@angular/core"; -import { Constructor, Opaque } from "type-fest"; - -import { SafeInjectionToken } from "../../services/injection-tokens"; - /** - * The return type of the {@link safeProvider} helper function. - * Used to distinguish a type safe provider definition from a non-type safe provider definition. + * @deprecated: Please use the SafeProvider & safeProvider from @bitwarden/ui-common */ -export type SafeProvider = Opaque; - -// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0 -type AbstractConstructor = abstract new (...args: any) => T; - -type MapParametersToDeps = { - [K in keyof T]: AbstractConstructor | SafeInjectionToken; -}; - -type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; - -/** - * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken - */ -type ProviderInstanceType = - T extends SafeInjectionToken - ? InstanceType> - : T extends Constructor | AbstractConstructor - ? InstanceType - : never; - -/** - * Represents a dependency provided with the useClass option. - */ -type SafeClassProvider< - A extends AbstractConstructor | SafeInjectionToken, - I extends Constructor>, - D extends MapParametersToDeps>, -> = { - provide: A; - useClass: I; - deps: D; -}; - -/** - * Represents a dependency provided with the useValue option. - */ -type SafeValueProvider, V extends SafeInjectionTokenType> = { - provide: A; - useValue: V; -}; - -/** - * Represents a dependency provided with the useFactory option. - */ -type SafeFactoryProvider< - A extends AbstractConstructor | SafeInjectionToken, - I extends (...args: any) => ProviderInstanceType, - D extends MapParametersToDeps>, -> = { - provide: A; - useFactory: I; - deps: D; - multi?: boolean; -}; - -/** - * Represents a dependency provided with the useExisting option. - */ -type SafeExistingProvider< - A extends Constructor | AbstractConstructor | SafeInjectionToken, - I extends Constructor> | AbstractConstructor>, -> = { - provide: A; - useExisting: I; -}; - -/** - * Represents a dependency where there is no abstract token, the token is the implementation - */ -type SafeConcreteProvider< - I extends Constructor, - D extends MapParametersToDeps>, -> = { - provide: I; - deps: D; -}; - -/** - * If useAngularDecorators: true is specified, do not require a deps array. - * This is a manual override for where @Injectable decorators are used - */ -type UseAngularDecorators = Omit & { - useAngularDecorators: true; -}; - -/** - * Represents a type with a deps array that may optionally be overridden with useAngularDecorators - */ -type AllowAngularDecorators = T | UseAngularDecorators; - -/** - * A factory function that creates a provider for the ngModule providers array. - * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. - * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, - * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. - * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) - * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) - * @returns The exact same object without modification (pass-through). - */ -export const safeProvider = < - // types for useClass - AClass extends AbstractConstructor | SafeInjectionToken, - IClass extends Constructor>, - DClass extends MapParametersToDeps>, - // types for useValue - AValue extends SafeInjectionToken, - VValue extends SafeInjectionTokenType, - // types for useFactory - AFactory extends AbstractConstructor | SafeInjectionToken, - IFactory extends (...args: any) => ProviderInstanceType, - DFactory extends MapParametersToDeps>, - // types for useExisting - AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, - IExisting extends - | Constructor> - | AbstractConstructor>, - // types for no token - IConcrete extends Constructor, - DConcrete extends MapParametersToDeps>, ->( - provider: - | AllowAngularDecorators> - | SafeValueProvider - | AllowAngularDecorators> - | SafeExistingProvider - | AllowAngularDecorators> - | Constructor, -): SafeProvider => provider as SafeProvider; +export { SafeProvider, safeProvider } from "@bitwarden/ui-common"; diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index b348a84d56f..01f983c25af 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { InjectionToken } from "@angular/core"; import { Observable, Subject } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -14,17 +13,9 @@ import { Theme } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message } from "@bitwarden/common/platform/messaging"; import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; - -declare const tag: unique symbol; -/** - * A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter. - * @remarks The default angular implementation does not use the generic type to define the structure of the object, - * so the structural type system will not complain about a mismatch in the type parameter. - * This is solved by assigning T to an arbitrary private property. - */ -export class SafeInjectionToken extends InjectionToken { - private readonly [tag]: T; -} +import { SafeInjectionToken } from "@bitwarden/ui-common"; +// Re-export the SafeInjectionToken from ui-common +export { SafeInjectionToken } from "@bitwarden/ui-common"; export const WINDOW = new SafeInjectionToken("WINDOW"); export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken< diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 79d1e38b21d..3d801f5ce0a 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -20,6 +20,7 @@ import { DefaultLoginComponentService, LoginDecryptionOptionsService, DefaultLoginDecryptionOptionsService, + DefaultLoginApprovalComponentService, } from "@bitwarden/auth/angular"; import { AuthRequestServiceAbstraction, @@ -34,11 +35,11 @@ import { UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, LogoutReason, - RegisterRouteService, AuthRequestApiService, DefaultAuthRequestApiService, DefaultLoginSuccessHandlerService, LoginSuccessHandlerService, + LoginApprovalComponentServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -138,11 +139,13 @@ import { import { AccountBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/account/account-billing-api.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { AccountBillingApiService } from "@bitwarden/common/billing/services/account/account-billing-api.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { BillingApiService } from "@bitwarden/common/billing/services/billing-api.service"; import { OrganizationBillingApiService } from "@bitwarden/common/billing/services/organization/organization-billing-api.service"; import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; +import { TaxService } from "@bitwarden/common/billing/services/tax.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; @@ -277,17 +280,19 @@ import { ImportServiceAbstraction, } from "@bitwarden/importer/core"; import { - KeyService as KeyServiceAbstraction, - DefaultKeyService as KeyService, + KeyService, + DefaultKeyService, BiometricStateService, DefaultBiometricStateService, - KdfConfigService, + BiometricsService, DefaultKdfConfigService, + KdfConfigService, UserAsymmetricKeysRegenerationService, DefaultUserAsymmetricKeysRegenerationService, UserAsymmetricKeysRegenerationApiService, DefaultUserAsymmetricKeysRegenerationApiService, } from "@bitwarden/key-management"; +import { SafeInjectionToken } from "@bitwarden/ui-common"; import { PasswordRepromptService } from "@bitwarden/vault"; import { VaultExportService, @@ -298,6 +303,8 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; @@ -316,7 +323,6 @@ import { MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, - SafeInjectionToken, SECURE_STORAGE, STATE_FACTORY, SYSTEM_LANGUAGE, @@ -407,7 +413,7 @@ const safeProviders: SafeProvider[] = [ deps: [ AccountServiceAbstraction, MessagingServiceAbstraction, - KeyServiceAbstraction, + KeyService, ApiServiceAbstraction, StateServiceAbstraction, TokenServiceAbstraction, @@ -419,7 +425,7 @@ const safeProviders: SafeProvider[] = [ deps: [ AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyServiceAbstraction, + KeyService, ApiServiceAbstraction, TokenServiceAbstraction, AppIdServiceAbstraction, @@ -454,10 +460,15 @@ const safeProviders: SafeProvider[] = [ useClass: CipherFileUploadService, deps: [ApiServiceAbstraction, FileUploadServiceAbstraction], }), + safeProvider({ + provide: DomainSettingsService, + useClass: DefaultDomainSettingsService, + deps: [StateProvider, ConfigService], + }), safeProvider({ provide: CipherServiceAbstraction, useFactory: ( - keyService: KeyServiceAbstraction, + keyService: KeyService, domainSettingsService: DomainSettingsService, apiService: ApiServiceAbstraction, i18nService: I18nServiceAbstraction, @@ -487,7 +498,7 @@ const safeProviders: SafeProvider[] = [ accountService, ), deps: [ - KeyServiceAbstraction, + KeyService, DomainSettingsService, ApiServiceAbstraction, I18nServiceAbstraction, @@ -506,7 +517,7 @@ const safeProviders: SafeProvider[] = [ provide: InternalFolderService, useClass: FolderService, deps: [ - KeyServiceAbstraction, + KeyService, EncryptService, I18nServiceAbstraction, CipherServiceAbstraction, @@ -551,7 +562,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CollectionService, useClass: DefaultCollectionService, - deps: [KeyServiceAbstraction, EncryptService, I18nServiceAbstraction, StateProvider], + deps: [KeyService, EncryptService, I18nServiceAbstraction, StateProvider], }), safeProvider({ provide: ENV_ADDITIONAL_REGIONS, @@ -596,8 +607,8 @@ const safeProviders: SafeProvider[] = [ deps: [CryptoFunctionServiceAbstraction], }), safeProvider({ - provide: KeyServiceAbstraction, - useClass: KeyService, + provide: KeyService, + useClass: DefaultKeyService, deps: [ PinServiceAbstraction, InternalMasterPasswordServiceAbstraction, @@ -622,7 +633,7 @@ const safeProviders: SafeProvider[] = [ useFactory: legacyPasswordGenerationServiceFactory, deps: [ EncryptService, - KeyServiceAbstraction, + KeyService, PolicyServiceAbstraction, AccountServiceAbstraction, StateProvider, @@ -631,7 +642,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: GeneratorHistoryService, useClass: LocalGeneratorHistoryService, - deps: [EncryptService, KeyServiceAbstraction, StateProvider], + deps: [EncryptService, KeyService, StateProvider], }), safeProvider({ provide: UsernameGenerationServiceAbstraction, @@ -639,7 +650,7 @@ const safeProviders: SafeProvider[] = [ deps: [ ApiServiceAbstraction, I18nServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, PolicyServiceAbstraction, AccountServiceAbstraction, @@ -679,7 +690,7 @@ const safeProviders: SafeProvider[] = [ provide: InternalSendService, useClass: SendService, deps: [ - KeyServiceAbstraction, + KeyService, I18nServiceAbstraction, KeyGenerationServiceAbstraction, SendStateProviderAbstraction, @@ -706,7 +717,7 @@ const safeProviders: SafeProvider[] = [ DomainSettingsService, InternalFolderService, CipherServiceAbstraction, - KeyServiceAbstraction, + KeyService, CollectionService, MessagingServiceAbstraction, InternalPolicyService, @@ -739,7 +750,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, - KeyServiceAbstraction, + KeyService, TokenServiceAbstraction, PolicyServiceAbstraction, BiometricStateService, @@ -766,6 +777,7 @@ const safeProviders: SafeProvider[] = [ StateEventRunnerService, TaskSchedulerService, LogService, + BiometricsService, LOCKED_CALLBACK, LOGOUT_CALLBACK, ], @@ -812,7 +824,7 @@ const safeProviders: SafeProvider[] = [ ImportApiServiceAbstraction, I18nServiceAbstraction, CollectionService, - KeyServiceAbstraction, + KeyService, EncryptService, PinServiceAbstraction, AccountServiceAbstraction, @@ -825,7 +837,7 @@ const safeProviders: SafeProvider[] = [ FolderServiceAbstraction, CipherServiceAbstraction, PinServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, CryptoFunctionServiceAbstraction, KdfConfigService, @@ -839,7 +851,7 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, ApiServiceAbstraction, PinServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, CryptoFunctionServiceAbstraction, CollectionService, @@ -946,7 +958,7 @@ const safeProviders: SafeProvider[] = [ deps: [ AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyServiceAbstraction, + KeyService, ApiServiceAbstraction, TokenServiceAbstraction, LogService, @@ -960,17 +972,15 @@ const safeProviders: SafeProvider[] = [ provide: UserVerificationServiceAbstraction, useClass: UserVerificationService, deps: [ - KeyServiceAbstraction, + KeyService, AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, PinServiceAbstraction, - LogService, - VaultTimeoutSettingsServiceAbstraction, - PlatformUtilsServiceAbstraction, KdfConfigService, + BiometricsService, ], }), safeProvider({ @@ -993,7 +1003,7 @@ const safeProviders: SafeProvider[] = [ deps: [ OrganizationApiServiceAbstraction, AccountServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, OrganizationUserApiService, I18nServiceAbstraction, @@ -1095,7 +1105,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DevicesServiceAbstraction, useClass: DevicesServiceImplementation, - deps: [DevicesApiServiceAbstraction], + deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction], }), safeProvider({ provide: DeviceTrustServiceAbstraction, @@ -1103,7 +1113,7 @@ const safeProviders: SafeProvider[] = [ deps: [ KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, AppIdServiceAbstraction, DevicesApiServiceAbstraction, @@ -1123,7 +1133,7 @@ const safeProviders: SafeProvider[] = [ AppIdServiceAbstraction, AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, ApiServiceAbstraction, StateProvider, @@ -1217,7 +1227,7 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, BillingApiServiceAbstraction, ConfigService, - KeyServiceAbstraction, + KeyService, EncryptService, I18nServiceAbstraction, OrganizationApiServiceAbstraction, @@ -1234,11 +1244,6 @@ const safeProviders: SafeProvider[] = [ useClass: BadgeSettingsService, deps: [StateProvider], }), - safeProvider({ - provide: DomainSettingsService, - useClass: DefaultDomainSettingsService, - deps: [StateProvider], - }), safeProvider({ provide: BiometricStateService, useClass: DefaultBiometricStateService, @@ -1264,10 +1269,15 @@ const safeProviders: SafeProvider[] = [ useClass: BillingApiService, deps: [ApiServiceAbstraction, LogService, ToastService], }), + safeProvider({ + provide: TaxServiceAbstraction, + useClass: TaxService, + deps: [ApiServiceAbstraction], + }), safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, - deps: [StateProvider], + deps: [StateProvider, PlatformUtilsServiceAbstraction, ApiServiceAbstraction], }), safeProvider({ provide: OrganizationManagementPreferencesService, @@ -1277,7 +1287,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: UserAutoUnlockKeyService, useClass: UserAutoUnlockKeyService, - deps: [KeyServiceAbstraction], + deps: [KeyService], }), safeProvider({ provide: ErrorHandler, @@ -1321,7 +1331,7 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultSetPasswordJitService, deps: [ ApiServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, I18nServiceAbstraction, KdfConfigService, @@ -1336,11 +1346,6 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultServerSettingsService, deps: [ConfigService], }), - safeProvider({ - provide: RegisterRouteService, - useClass: RegisterRouteService, - deps: [ConfigService], - }), safeProvider({ provide: AnonLayoutWrapperDataService, useClass: DefaultAnonLayoutWrapperDataService, @@ -1349,7 +1354,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: RegistrationFinishServiceAbstraction, useClass: DefaultRegistrationFinishService, - deps: [KeyServiceAbstraction, AccountApiServiceAbstraction], + deps: [KeyService, AccountApiServiceAbstraction], }), safeProvider({ provide: ViewCacheService, @@ -1376,7 +1381,7 @@ const safeProviders: SafeProvider[] = [ PlatformUtilsServiceAbstraction, AccountServiceAbstraction, KdfConfigService, - KeyServiceAbstraction, + KeyService, ], }), safeProvider({ @@ -1389,6 +1394,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultAuthRequestApiService, deps: [ApiServiceAbstraction, LogService], }), + safeProvider({ + provide: LoginApprovalComponentServiceAbstraction, + useClass: DefaultLoginApprovalComponentService, + deps: [], + }), safeProvider({ provide: LoginDecryptionOptionsService, useClass: DefaultLoginDecryptionOptionsService, @@ -1404,7 +1414,7 @@ const safeProviders: SafeProvider[] = [ provide: UserAsymmetricKeysRegenerationService, useClass: DefaultUserAsymmetricKeysRegenerationService, deps: [ - KeyServiceAbstraction, + KeyService, CipherServiceAbstraction, UserAsymmetricKeysRegenerationApiService, LogService, diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index 228abec98a9..aeee1fa104c 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -3,7 +3,15 @@ import { DatePipe } from "@angular/common"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } from "rxjs"; +import { + Subject, + firstValueFrom, + takeUntil, + map, + BehaviorSubject, + concatMap, + switchMap, +} from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -197,8 +205,13 @@ export class AddEditComponent implements OnInit, OnDestroy { const env = await firstValueFrom(this.environmentService.environment$); this.sendLinkBaseUrl = env.getSendUrl(); - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntil(this.destroy$), + ) .subscribe((hasPremiumFromAnySource) => { this.canAccessPremium = hasPremiumFromAnySource; }); diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 3a9d0289971..b86d48d3911 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -16,7 +16,7 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin- import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; -import { ClientType, EventType } from "@bitwarden/common/enums"; +import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -24,6 +24,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -40,7 +41,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { generate_ssh_key } from "@bitwarden/sdk-internal"; import { PasswordRepromptService } from "@bitwarden/vault"; @Directive() @@ -102,6 +104,8 @@ export class AddEditComponent implements OnInit, OnDestroy { private personalOwnershipPolicyAppliesToActiveUser: boolean; private previousCipherId: string; + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); const creationDate = this.datePipe.transform( @@ -124,12 +128,14 @@ export class AddEditComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected logService: LogService, protected passwordRepromptService: PasswordRepromptService, - private organizationService: OrganizationService, + protected organizationService: OrganizationService, protected dialogService: DialogService, protected win: Window, protected datePipe: DatePipe, protected configService: ConfigService, protected cipherAuthorizationService: CipherAuthorizationService, + protected toastService: ToastService, + private sdkService: SdkService, ) { this.typeOptions = [ { name: i18nService.t("typeLogin"), value: CipherType.Login }, @@ -206,7 +212,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.canUseReprompt = await this.passwordRepromptService.enabled(); const sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem); - if (this.platformUtilsService.getClientType() == ClientType.Desktop && sshKeysEnabled) { + if (sshKeysEnabled) { this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey }); } } @@ -259,12 +265,10 @@ export class AddEditComponent implements OnInit, OnDestroy { const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo(); + const activeUserId = await firstValueFrom(this.activeUserId$); if (this.cipher == null) { if (this.editMode) { const cipher = await this.loadCipher(); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); @@ -323,7 +327,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.cipher.login.fido2Credentials = null; } - this.folders$ = this.folderService.folderViews$; + this.folders$ = this.folderService.folderViews$(activeUserId); if (this.editMode && this.previousCipherId !== this.cipherId) { void this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, [this.cipher]); @@ -339,6 +343,17 @@ export class AddEditComponent implements OnInit, OnDestroy { [this.collectionId as CollectionId], this.isAdminConsoleAction, ); + + if (!this.editMode || this.cloneMode) { + // Creating an ssh key directly while filtering to the ssh key category + // must force a key to be set. SSH keys must never be created with an empty private key field + if ( + this.cipher.type === CipherType.SshKey && + (this.cipher.sshKey.privateKey == null || this.cipher.sshKey.privateKey === "") + ) { + await this.generateSshKey(false); + } + } } async submit(): Promise { @@ -357,11 +372,11 @@ export class AddEditComponent implements OnInit, OnDestroy { } if (this.cipher.name == null || this.cipher.name === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nameRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nameRequired"), + }); return false; } @@ -370,11 +385,11 @@ export class AddEditComponent implements OnInit, OnDestroy { !this.allowPersonal && this.cipher.organizationId == null ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("personalOwnershipSubmitError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("personalOwnershipSubmitError"), + }); return false; } @@ -409,11 +424,11 @@ export class AddEditComponent implements OnInit, OnDestroy { this.formPromise = this.saveCipher(cipher); await this.formPromise; this.cipher.id = cipher.id; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode && !this.cloneMode ? "editedItem" : "addedItem"), + }); this.onSavedCipher.emit(this.cipher); this.messagingService.send(this.editMode && !this.cloneMode ? "editedCipher" : "addedCipher"); return true; @@ -499,11 +514,13 @@ export class AddEditComponent implements OnInit, OnDestroy { try { this.deletePromise = this.deleteCipher(); await this.deletePromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); this.onDeletedCipher.emit(this.cipher); this.messagingService.send( this.cipher.isDeleted ? "permanentlyDeletedCipher" : "deletedCipher", @@ -523,7 +540,11 @@ export class AddEditComponent implements OnInit, OnDestroy { try { this.restorePromise = this.restoreCipher(); await this.restorePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); this.onRestoredCipher.emit(this.cipher); this.messagingService.send("restoredCipher"); } catch (e) { @@ -664,13 +685,17 @@ export class AddEditComponent implements OnInit, OnDestroy { this.checkPasswordPromise = null; if (matches > 0) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("passwordExposed", matches.toString()), - ); + this.toastService.showToast({ + variant: "warning", + title: null, + message: this.i18nService.t("passwordExposed", matches.toString()), + }); } else { - this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordSafe"), + }); } } @@ -764,11 +789,11 @@ export class AddEditComponent implements OnInit, OnDestroy { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), + }); if (typeI18nKey === "password") { void this.eventCollectionService.collectMany(EventType.Cipher_ClientCopiedPassword, [ @@ -786,4 +811,26 @@ export class AddEditComponent implements OnInit, OnDestroy { return true; } + + private async generateSshKey(showNotification: boolean = true) { + await firstValueFrom(this.sdkService.client$); + const sshKey = generate_ssh_key("Ed25519"); + this.cipher.sshKey.privateKey = sshKey.private_key; + this.cipher.sshKey.publicKey = sshKey.public_key; + this.cipher.sshKey.keyFingerprint = sshKey.key_fingerprint; + + if (showNotification) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } + } + + async typeChange() { + if (this.cipher.type === CipherType.SshKey) { + await this.generateSshKey(); + } + } } diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index 521d38a1f47..a3b635f151d 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -64,21 +64,21 @@ export class AttachmentsComponent implements OnInit { const fileEl = document.getElementById("file") as HTMLInputElement; const files = fileEl.files; if (files == null || files.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("selectFile"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); return; } if (files[0].size > 524288000) { // 500 MB - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("maxFileSize"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("maxFileSize"), + }); return; } @@ -91,7 +91,11 @@ export class AttachmentsComponent implements OnInit { this.cipher = await this.cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), ); - this.platformUtilsService.showToast("success", null, this.i18nService.t("attachmentSaved")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("attachmentSaved"), + }); this.onUploadedAttachment.emit(); } catch (e) { this.logService.error(e); @@ -122,7 +126,11 @@ export class AttachmentsComponent implements OnInit { try { this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); await this.deletePromises[attachment.id]; - this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedAttachment")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedAttachment"), + }); const i = this.cipher.attachments.indexOf(attachment); if (i > -1) { this.cipher.attachments.splice(i, 1); @@ -142,11 +150,11 @@ export class AttachmentsComponent implements OnInit { } if (!this.canAccessAttachments) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("premiumRequired"), - this.i18nService.t("premiumRequiredDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("premiumRequired"), + message: this.i18nService.t("premiumRequiredDesc"), + }); return; } @@ -171,7 +179,11 @@ export class AttachmentsComponent implements OnInit { a.downloading = true; const response = await fetch(new Request(url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -192,8 +204,14 @@ export class AttachmentsComponent implements OnInit { title: null, message: this.i18nService.t("fileSavedToDevice"), }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; @@ -209,7 +227,7 @@ export class AttachmentsComponent implements OnInit { ); const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; @@ -241,7 +259,11 @@ export class AttachmentsComponent implements OnInit { a.downloading = true; const response = await fetch(new Request(attachment.url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -279,14 +301,20 @@ export class AttachmentsComponent implements OnInit { } } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("attachmentSaved"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("attachmentSaved"), + }); this.onReuploadedAttachment.emit(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; diff --git a/libs/angular/src/vault/components/folder-add-edit.component.ts b/libs/angular/src/vault/components/folder-add-edit.component.ts index 6d9ae53cc66..28ed0dc2aed 100644 --- a/libs/angular/src/vault/components/folder-add-edit.component.ts +++ b/libs/angular/src/vault/components/folder-add-edit.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Validators, FormBuilder } from "@angular/forms"; -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -11,7 +11,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @Directive() @@ -27,6 +27,8 @@ export class FolderAddEditComponent implements OnInit { deletePromise: Promise; protected componentName = ""; + protected activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + formGroup = this.formBuilder.group({ name: ["", [Validators.required]], }); @@ -41,6 +43,7 @@ export class FolderAddEditComponent implements OnInit { protected logService: LogService, protected dialogService: DialogService, protected formBuilder: FormBuilder, + protected toastService: ToastService, ) {} async ngOnInit() { @@ -50,25 +53,25 @@ export class FolderAddEditComponent implements OnInit { async submit(): Promise { this.folder.name = this.formGroup.controls.name.value; if (this.folder.name == null || this.folder.name === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("nameRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("nameRequired"), + }); return false; } try { - const activeAccountId = await firstValueFrom(this.accountService.activeAccount$); - const userKey = await this.keyService.getUserKeyWithLegacySupport(activeAccountId.id); + const activeUserId = await firstValueFrom(this.activeUserId$); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); const folder = await this.folderService.encrypt(this.folder, userKey); - this.formPromise = this.folderApiService.save(folder); + this.formPromise = this.folderApiService.save(folder, activeUserId); await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.editMode ? "editedFolder" : "addedFolder"), + }); this.onSavedFolder.emit(this.folder); return true; } catch (e) { @@ -90,9 +93,14 @@ export class FolderAddEditComponent implements OnInit { } try { - this.deletePromise = this.folderApiService.delete(this.folder.id); + const activeUserId = await firstValueFrom(this.activeUserId$); + this.deletePromise = this.folderApiService.delete(this.folder.id, activeUserId); await this.deletePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("deletedFolder")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedFolder"), + }); this.onDeletedFolder.emit(this.folder); } catch (e) { this.logService.error(e); @@ -107,8 +115,10 @@ export class FolderAddEditComponent implements OnInit { if (this.editMode) { this.editMode = true; this.title = this.i18nService.t("editFolder"); - const folder = await this.folderService.get(this.folderId); - this.folder = await folder.decrypt(); + const activeUserId = await firstValueFrom(this.activeUserId$); + this.folder = await firstValueFrom( + this.folderService.getDecrypted$(this.folderId, activeUserId), + ); } else { this.title = this.i18nService.t("addFolder"); } diff --git a/libs/angular/src/vault/components/icon.component.html b/libs/angular/src/vault/components/icon.component.html index 976c6ea421d..f16545617c9 100644 --- a/libs/angular/src/vault/components/icon.component.html +++ b/libs/angular/src/vault/components/icon.component.html @@ -4,13 +4,13 @@ [src]="data.image" [appFallbackSrc]="data.fallbackImage" *ngIf="data.imageEnabled && data.image" - class="tw-max-h-6 tw-max-w-6 tw-rounded-md" + class="tw-h-6 tw-w-6 tw-rounded-md" alt="" decoding="async" loading="lazy" /> diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 942a34c58bb..0b385688d0b 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; +import { ToastService } from "@bitwarden/components"; @Directive() export class PasswordHistoryComponent implements OnInit { @@ -20,6 +21,7 @@ export class PasswordHistoryComponent implements OnInit { protected i18nService: I18nService, protected accountService: AccountService, private win: Window, + private toastService: ToastService, ) {} async ngOnInit() { @@ -29,11 +31,11 @@ export class PasswordHistoryComponent implements OnInit { copy(password: string) { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(password, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t("password")), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t("password")), + }); } protected async init() { diff --git a/libs/angular/src/vault/components/premium.component.ts b/libs/angular/src/vault/components/premium.component.ts index 2ad25f2e45a..e86c6beda47 100644 --- a/libs/angular/src/vault/components/premium.component.ts +++ b/libs/angular/src/vault/components/premium.component.ts @@ -1,9 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { OnInit, Directive } from "@angular/core"; -import { firstValueFrom, Observable } from "rxjs"; +import { firstValueFrom, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -11,7 +12,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; @Directive() export class PremiumComponent implements OnInit { @@ -30,8 +31,14 @@ export class PremiumComponent implements OnInit { protected dialogService: DialogService, private environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, + accountService: AccountService, ) { - this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.isPremium$ = accountService.activeAccount$.pipe( + switchMap((account) => + billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); } async ngOnInit() { @@ -45,7 +52,11 @@ export class PremiumComponent implements OnInit { try { this.refreshPromise = this.apiService.refreshIdentityToken(); await this.refreshPromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("refreshComplete")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("refreshComplete"), + }); } catch (e) { this.logService.error(e); } diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 052ab95f3bc..4ef00e90063 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs"; +import { BehaviorSubject, firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -118,6 +118,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected async doSearch(indexedCiphers?: CipherView[]) { indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); + + const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); + if (failedCiphers != null && failedCiphers.length > 0) { + indexedCiphers = [...failedCiphers, ...indexedCiphers]; + } + this.ciphers = await this.searchService.searchCiphers( this.searchText, [this.filter, this.deletedFilter], diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index a6b9a571730..18caa875e03 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -40,7 +40,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -65,6 +65,7 @@ export class ViewComponent implements OnDestroy, OnInit { showPrivateKey: boolean; canAccessPremium: boolean; showPremiumRequiredTotp: boolean; + showUpgradeRequiredTotp: boolean; totpCode: string; totpCodeFormatted: string; totpDash: number; @@ -79,6 +80,8 @@ export class ViewComponent implements OnDestroy, OnInit { private previousCipherId: string; private passwordReprompted = false; + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); const creationDate = this.datePipe.transform( @@ -112,6 +115,7 @@ export class ViewComponent implements OnDestroy, OnInit { protected datePipe: DatePipe, protected accountService: AccountService, private billingAccountProfileStateService: BillingAccountProfileStateService, + protected toastService: ToastService, private cipherAuthorizationService: CipherAuthorizationService, ) {} @@ -141,32 +145,33 @@ export class ViewComponent implements OnDestroy, OnInit { this.cleanUp(); const cipher = await this.cipherService.get(this.cipherId); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); this.canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); this.showPremiumRequiredTotp = - this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; + this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId; this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ this.collectionId as CollectionId, ]); + this.showUpgradeRequiredTotp = + this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp; + if (this.cipher.folderId) { this.folder = await ( - await firstValueFrom(this.folderService.folderViews$) + await firstValueFrom(this.folderService.folderViews$(activeUserId)) ).find((f) => f.id == this.cipher.folderId); } - if ( - this.cipher.type === CipherType.Login && - this.cipher.login.totp && - (cipher.organizationUseTotp || this.canAccessPremium) - ) { + const canGenerateTotp = this.cipher.organizationId + ? this.cipher.organizationUseTotp + : this.canAccessPremium; + + if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) { await this.totpUpdateCode(); const interval = this.totpService.getTimeInterval(this.cipher.login.totp); await this.totpTick(interval); @@ -242,11 +247,13 @@ export class ViewComponent implements OnDestroy, OnInit { try { await this.deleteCipher(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem", + ), + }); this.onDeletedCipher.emit(this.cipher); } catch (e) { this.logService.error(e); @@ -262,7 +269,11 @@ export class ViewComponent implements OnDestroy, OnInit { try { await this.restoreCipher(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("restoredItem")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); this.onRestoredCipher.emit(this.cipher); } catch (e) { this.logService.error(e); @@ -345,13 +356,17 @@ export class ViewComponent implements OnDestroy, OnInit { const matches = await this.checkPasswordPromise; if (matches > 0) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("passwordExposed", matches.toString()), - ); + this.toastService.showToast({ + variant: "warning", + title: null, + message: this.i18nService.t("passwordExposed", matches.toString()), + }); } else { - this.platformUtilsService.showToast("success", null, this.i18nService.t("passwordSafe")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordSafe"), + }); } } @@ -381,11 +396,11 @@ export class ViewComponent implements OnDestroy, OnInit { const copyOptions = this.win != null ? { window: this.win } : null; this.platformUtilsService.copyToClipboard(value, copyOptions); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)), + }); if (typeI18nKey === "password") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -418,11 +433,11 @@ export class ViewComponent implements OnDestroy, OnInit { } if (this.cipher.organizationId == null && !this.canAccessPremium) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("premiumRequired"), - this.i18nService.t("premiumRequiredDesc"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("premiumRequired"), + message: this.i18nService.t("premiumRequiredDesc"), + }); return; } @@ -446,7 +461,11 @@ export class ViewComponent implements OnDestroy, OnInit { a.downloading = true; const response = await fetch(new Request(url, { cache: "no-store" })); if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); a.downloading = false; return; } @@ -462,8 +481,14 @@ export class ViewComponent implements OnDestroy, OnInit { fileName: attachment.fileName, blobData: decBuf, }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } a.downloading = false; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts index e278113a653..ba19cf808ee 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts @@ -9,6 +9,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts index 20550e0e8cf..8b406877a12 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts @@ -9,6 +9,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index a2b624876be..260780e1964 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Injectable } from "@angular/core"; -import { firstValueFrom, from, map, mergeMap, Observable } from "rxjs"; +import { firstValueFrom, from, map, mergeMap, Observable, switchMap } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { @@ -11,6 +11,7 @@ import { import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -21,6 +22,8 @@ import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service"; import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { COLLAPSED_GROUPINGS } from "./../../../../../common/src/vault/services/key-state/collapsed-groupings.state"; const NestingDelimiter = "/"; @@ -32,6 +35,8 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti private readonly collapsedGroupings$: Observable> = this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c))); + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + constructor( protected organizationService: OrganizationService, protected folderService: FolderService, @@ -39,6 +44,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti protected collectionService: CollectionService, protected policyService: PolicyService, protected stateProvider: StateProvider, + protected accountService: AccountService, ) {} async storeCollapsedFilterNodes(collapsedFilterNodes: Set): Promise { @@ -81,7 +87,8 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti }); }; - return this.folderService.folderViews$.pipe( + return this.activeUserId$.pipe( + switchMap((userId) => this.folderService.folderViews$(userId)), mergeMap((folders) => from(transformation(folders))), ); } @@ -126,8 +133,9 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async getFolderNested(id: string): Promise> { + const activeUserId = await firstValueFrom(this.activeUserId$); const folders = await this.getAllFoldersNested( - await firstValueFrom(this.folderService.folderViews$), + await firstValueFrom(this.folderService.folderViews$(activeUserId)), ); return ServiceUtils.getTreeNodeObjectFromList(folders, id) as TreeNode; } diff --git a/libs/angular/tsconfig.json b/libs/angular/tsconfig.json index 6004a56fb55..b638410a6a8 100644 --- a/libs/angular/tsconfig.json +++ b/libs/angular/tsconfig.json @@ -1,5 +1,26 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/angular": ["../auth/src/angular"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-components": ["../tools/generator/components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/importer/core": ["../importer/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault": ["../vault/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/auth/jest.config.js b/libs/auth/jest.config.js index 8bc834c7dab..121d423be17 100644 --- a/libs/auth/jest.config.js +++ b/libs/auth/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts index b07504b7c8d..9f504c75d29 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts @@ -18,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ButtonModule } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests"; import { LockIcon } from "../icons"; import { RegistrationCheckEmailIcon } from "../icons/registration-check-email.icon"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index 91229f38ab2..05ddb9614f1 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -9,8 +9,14 @@ import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IconModule, Icon } from "../../../../components/src/icon"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SharedModule } from "../../../../components/src/shared"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../components/src/typography"; import { BitwardenLogo, BitwardenShield } from "../icons"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index 27eb27c53b9..c7e15d9dcfa 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -7,7 +7,11 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service"; import { LockIcon } from "../icons"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index 817687ef2bc..66111f3e5af 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -57,10 +57,6 @@ export * from "./user-verification/user-verification-dialog.component"; export * from "./user-verification/user-verification-dialog.types"; export * from "./user-verification/user-verification-form-input.component"; -// lock -export * from "./lock/lock.component"; -export * from "./lock/lock-component.service"; - // vault timeout export * from "./vault-timeout-input/vault-timeout-input.component"; diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 94baecb9ef2..c613cf5f533 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -26,7 +26,11 @@ import { } from "@bitwarden/components"; import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InputsFieldMatch } from "../../../../angular/src/auth/validators/inputs-field-match.validator"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SharedModule } from "../../../../components/src/shared"; import { PasswordCalloutComponent } from "../password-callout/password-callout.component"; diff --git a/libs/auth/src/angular/input-password/input-password.stories.ts b/libs/auth/src/angular/input-password/input-password.stories.ts index 99c0aba81b8..41577328f87 100644 --- a/libs/auth/src/angular/input-password/input-password.stories.ts +++ b/libs/auth/src/angular/input-password/input-password.stories.ts @@ -14,7 +14,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests"; import { InputPasswordComponent } from "./input-password.component"; diff --git a/libs/auth/src/angular/login-approval/login-approval.component.html b/libs/auth/src/angular/login-approval/login-approval.component.html index ddbc48d71a3..c0cb9b9caf4 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.html +++ b/libs/auth/src/angular/login-approval/login-approval.component.html @@ -1,23 +1,31 @@ {{ "areYouTryingtoLogin" | i18n }} -

    {{ "logInAttemptBy" | i18n: email }}

    -
    - {{ "fingerprintPhraseHeader" | i18n }} -

    {{ fingerprintPhrase }}

    -
    -
    - {{ "deviceType" | i18n }} -

    {{ authRequestResponse?.requestDeviceType }}

    -
    -
    - {{ "ipAddress" | i18n }} -

    {{ authRequestResponse?.requestIpAddress }}

    -
    -
    - {{ "time" | i18n }} -

    {{ requestTimeText }}

    -
    + +
    + +
    +
    + + +

    {{ "logInAttemptBy" | i18n: email }}

    +
    + {{ "fingerprintPhraseHeader" | i18n }} +

    {{ fingerprintPhrase }}

    +
    +
    + {{ "deviceType" | i18n }} +

    {{ authRequestResponse?.requestDeviceType }}

    +
    +
    + {{ "ipAddress" | i18n }} +

    {{ authRequestResponse?.requestIpAddress }}

    +
    +
    + {{ "time" | i18n }} +

    {{ requestTimeText }}

    +
    +
    @@ -34,7 +42,7 @@ type="button" buttonType="secondary" [bitAction]="denyLogin" - [bitDialogClose]="true" + [disabled]="loading" > {{ "denyLogIn" | i18n }} diff --git a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts index ff598bdeb91..da30df62fff 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts @@ -13,6 +13,7 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -29,6 +30,7 @@ describe("LoginApprovalComponent", () => { let i18nService: MockProxy; let dialogRef: MockProxy; let toastService: MockProxy; + let validationService: MockProxy; const testNotificationId = "test-notification-id"; const testEmail = "test@bitwarden.com"; @@ -41,6 +43,7 @@ describe("LoginApprovalComponent", () => { i18nService = mock(); dialogRef = mock(); toastService = mock(); + validationService = mock(); accountService.activeAccount$ = of({ email: testEmail, @@ -62,6 +65,7 @@ describe("LoginApprovalComponent", () => { { provide: KeyService, useValue: mock() }, { provide: DialogRef, useValue: dialogRef }, { provide: ToastService, useValue: toastService }, + { provide: ValidationService, useValue: validationService }, { provide: LoginApprovalComponentServiceAbstraction, useValue: mock(), diff --git a/libs/auth/src/angular/login-approval/login-approval.component.ts b/libs/auth/src/angular/login-approval/login-approval.component.ts index 5192334a0ca..3b44f545abb 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.ts @@ -16,6 +16,7 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { AsyncActionsModule, @@ -40,6 +41,8 @@ export interface LoginApprovalDialogParams { imports: [CommonModule, AsyncActionsModule, ButtonModule, DialogModule, JslibModule], }) export class LoginApprovalComponent implements OnInit, OnDestroy { + loading = true; + notificationId: string; private destroy$ = new Subject(); @@ -62,25 +65,25 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { private dialogRef: DialogRef, private toastService: ToastService, private loginApprovalComponentService: LoginApprovalComponentService, + private validationService: ValidationService, ) { this.notificationId = params.notificationId; } async ngOnDestroy(): Promise { clearInterval(this.interval); - const closedWithButton = await firstValueFrom(this.dialogRef.closed); - if (!closedWithButton) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.retrieveAuthRequestAndRespond(false); - } this.destroy$.next(); this.destroy$.complete(); } async ngOnInit() { if (this.notificationId != null) { - this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); + try { + this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); + } catch (error) { + this.validationService.showError(error); + } + const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey); this.email = await await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), @@ -96,6 +99,8 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { }, RequestTimeUpdate); this.loginApprovalComponentService.showLoginRequestedAlertIfWindowNotVisible(this.email); + + this.loading = false; } } @@ -131,6 +136,8 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { ); this.showResultToast(loginResponse); } + + this.dialogRef.close(approve); } showResultToast(loginResponse: AuthRequestResponse) { diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 5600077c363..a3f5e062e4f 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -107,7 +107,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private validationService: ValidationService, ) { - this.clientType === this.platformUtilsService.getClientType(); + this.clientType = this.platformUtilsService.getClientType(); } async ngOnInit() { diff --git a/libs/auth/src/angular/login/login-secondary-content.component.ts b/libs/auth/src/angular/login/login-secondary-content.component.ts index dbc9535e67a..b608542b375 100644 --- a/libs/auth/src/angular/login/login-secondary-content.component.ts +++ b/libs/auth/src/angular/login/login-secondary-content.component.ts @@ -3,7 +3,6 @@ import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { RegisterRouteService } from "@bitwarden/auth/common"; import { DefaultServerSettingsService } from "@bitwarden/common/platform/services/default-server-settings.service"; import { LinkModule } from "@bitwarden/components"; @@ -13,16 +12,12 @@ import { LinkModule } from "@bitwarden/components"; template: `
    `, }) export class LoginSecondaryContentComponent { - registerRouteService = inject(RegisterRouteService); serverSettingsService = inject(DefaultServerSettingsService); - // TODO: remove when email verification flag is removed - protected registerRoute$ = this.registerRouteService.registerRoute$(); - protected isUserRegistrationDisabled$ = this.serverSettingsService.isUserRegistrationDisabled$; } diff --git a/libs/auth/src/angular/login/login.component.html b/libs/auth/src/angular/login/login.component.html index 54a04d3de6c..c7837db74f2 100644 --- a/libs/auth/src/angular/login/login.component.html +++ b/libs/auth/src/angular/login/login.component.html @@ -20,8 +20,8 @@ formControlName="email" bitInput appAutofocus - (blur)="onEmailBlur($event)" - (keyup.enter)="continue()" + (input)="onEmailInput($event)" + (keyup.enter)="continuePressed()" /> @@ -33,7 +33,7 @@
    - @@ -54,33 +54,10 @@ - - - - {{ "useSingleSignOn" | i18n }} - - - - - +
    diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 33c167dcaed..f9aaa5d1e05 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; @@ -12,7 +10,6 @@ import { LoginStrategyServiceAbstraction, LoginSuccessHandlerService, PasswordLoginCredentials, - RegisterRouteService, } from "@bitwarden/auth/common"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; @@ -72,16 +69,15 @@ export enum LoginUiState { ], }) export class LoginComponent implements OnInit, OnDestroy { - @ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef; + @ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined; private destroy$ = new Subject(); - private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; + private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined; readonly Icons = { WaveIcon, VaultIcon }; clientType: ClientType; ClientType = ClientType; LoginUiState = LoginUiState; - registerRoute$ = this.registerRouteService.registerRoute$(); // TODO: remove when email verification flag is removed isKnownDevice = false; loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY; @@ -97,13 +93,13 @@ export class LoginComponent implements OnInit, OnDestroy { { updateOn: "submit" }, ); - get emailFormControl(): FormControl { + get emailFormControl(): FormControl { return this.formGroup.controls.email; } // Web properties - enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; - policies: Policy[]; + enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined; + policies: Policy[] | undefined; showResetPasswordAutoEnrollWarning = false; // Desktop properties @@ -125,7 +121,6 @@ export class LoginComponent implements OnInit, OnDestroy { private passwordStrengthService: PasswordStrengthServiceAbstraction, private platformUtilsService: PlatformUtilsService, private policyService: InternalPolicyService, - private registerRouteService: RegisterRouteService, private router: Router, private toastService: ToastService, private logService: LogService, @@ -200,12 +195,12 @@ export class LoginComponent implements OnInit, OnDestroy { return; } - const credentials = new PasswordLoginCredentials( - email, - masterPassword, - null, // captcha no longer used in new login / registration scenarios - null, - ); + if (!email || !masterPassword) { + this.logService.error("Email and master password are required"); + return; + } + + const credentials = new PasswordLoginCredentials(email, masterPassword); try { const authResult = await this.loginStrategyService.logIn(credentials); @@ -301,7 +296,12 @@ export class LoginComponent implements OnInit, OnDestroy { } protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise { - await this.loginComponentService.launchSsoBrowserWindow(this.emailFormControl.value, clientId); + const email = this.emailFormControl.value; + if (!email) { + this.logService.error("Email is required for SSO login"); + return; + } + await this.loginComponentService.launchSsoBrowserWindow(email, clientId); } protected async evaluatePassword(): Promise { @@ -337,9 +337,14 @@ export class LoginComponent implements OnInit, OnDestroy { const masterPassword = this.formGroup.controls.masterPassword.value; + // Return false if masterPassword is null/undefined since this is only evaluated after successful login + if (!masterPassword) { + return false; + } + const passwordStrength = this.passwordStrengthService.getPasswordStrength( masterPassword, - this.formGroup.value.email, + this.formGroup.value.email ?? undefined, )?.score; return !this.policyService.evaluateMasterPassword( @@ -363,6 +368,7 @@ export class LoginComponent implements OnInit, OnDestroy { protected async validateEmail(): Promise { this.formGroup.controls.email.markAsTouched(); + this.formGroup.controls.email.updateValueAndValidity({ onlySelf: true, emitEvent: true }); return this.formGroup.controls.email.valid; } @@ -404,7 +410,10 @@ export class LoginComponent implements OnInit, OnDestroy { } // Check to see if the device is known so we can show the Login with Device option - await this.getKnownDevice(this.emailFormControl.value); + const email = this.emailFormControl.value; + if (email) { + await this.getKnownDevice(email); + } } } @@ -412,11 +421,10 @@ export class LoginComponent implements OnInit, OnDestroy { * Set the email value from the input field. * @param event The event object from the input field. */ - onEmailBlur(event: Event) { + onEmailInput(event: Event) { const emailInput = event.target as HTMLInputElement; this.formGroup.controls.email.setValue(emailInput.value); - // Call setLoginEmail so that the email is pre-populated when navigating to the "enter password" screen. - this.loginEmailService.setLoginEmail(this.formGroup.value.email); + this.loginEmailService.setLoginEmail(emailInput.value); } isLoginWithPasskeySupported() { @@ -428,28 +436,36 @@ export class LoginComponent implements OnInit, OnDestroy { await this.router.navigateByUrl("/hint"); } - protected async goToRegister(): Promise { - // TODO: remove when email verification flag is removed - const registerRoute = await firstValueFrom(this.registerRoute$); - - if (this.emailFormControl.valid) { - await this.router.navigate([registerRoute], { - queryParams: { email: this.emailFormControl.value }, - }); + protected async saveEmailSettings(): Promise { + const email = this.formGroup.value.email; + if (!email) { + this.logService.error("Email is required to save email settings."); return; } - await this.router.navigate([registerRoute]); - } - - protected async saveEmailSettings(): Promise { - await this.loginEmailService.setLoginEmail(this.formGroup.value.email); - this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); + await this.loginEmailService.setLoginEmail(email); + this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail ?? false); await this.loginEmailService.saveEmailSettings(); } + /** + * Continue button clicked (or enter key pressed). + * Adds the login url to the browser's history so that the back button can be used to go back to the email entry state. + * Needs to be separate from the continue() function because that can be triggered by the browser's forward button. + */ + protected async continuePressed() { + // Add a new entry to the browser's history so that there is a history entry to go back to + history.pushState({}, "", window.location.href); + await this.continue(); + } + + /** + * Continue to the master password entry state (only if email is validated) + */ protected async continue(): Promise { - if (await this.validateEmail()) { + const isEmailValid = await this.validateEmail(); + + if (isEmailValid) { await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY); } } @@ -460,9 +476,16 @@ export class LoginComponent implements OnInit, OnDestroy { * @param email - The user's email */ private async getKnownDevice(email: string): Promise { + if (!email) { + this.isKnownDevice = false; + return; + } + try { const deviceIdentifier = await this.appIdService.getAppId(); this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.isKnownDevice = false; } @@ -503,7 +526,7 @@ export class LoginComponent implements OnInit, OnDestroy { const orgPolicies = await this.loginComponentService.getOrgPolicies(); this.policies = orgPolicies?.policies; - this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled; + this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled ?? false; let paramEmailIsSet = false; @@ -525,7 +548,9 @@ export class LoginComponent implements OnInit, OnDestroy { } // Check to see if the device is known so that we can show the Login with Device option - await this.getKnownDevice(this.emailFormControl.value); + if (this.emailFormControl.value) { + await this.getKnownDevice(this.emailFormControl.value); + } // Backup check to handle unknown case where activatedRoute is not available // This shouldn't happen under normal circumstances @@ -573,23 +598,50 @@ export class LoginComponent implements OnInit, OnDestroy { * Handle the back button click to transition back to the email entry state. */ protected async backButtonClicked() { - // Replace the history so the "forward" button doesn't show (which wouldn't do anything) - history.pushState(null, "", window.location.pathname); - await this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY); + history.back(); } /** * Handle the popstate event to transition back to the email entry state when the back button is clicked. + * Also handles the case where the user clicks the forward button. * @param event - The popstate event. */ - private handlePopState = (event: PopStateEvent) => { + private handlePopState = async (event: PopStateEvent) => { if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) { - // Prevent default navigation + // Prevent default navigation when the browser's back button is clicked event.preventDefault(); - // Replace the history so the "forward" button doesn't show (which wouldn't do anything) - history.pushState(null, "", window.location.pathname); // Transition back to email entry state void this.toggleLoginUiState(LoginUiState.EMAIL_ENTRY); + } else if (this.loginUiState === LoginUiState.EMAIL_ENTRY) { + // Prevent default navigation when the browser's forward button is clicked + event.preventDefault(); + // Continue to the master password entry state + await this.continue(); } }; + + /** + * Handle the SSO button click. + * @param event - The event object. + */ + async handleSsoClick() { + const isEmailValid = await this.validateEmail(); + + if (!isEmailValid) { + return; + } + + await this.saveEmailSettings(); + + if (this.clientType === ClientType.Web) { + await this.router.navigate(["/sso"], { + queryParams: { email: this.formGroup.value.email }, + }); + return; + } + + await this.launchSsoBrowserWindow( + this.clientType === ClientType.Browser ? "browser" : "desktop", + ); + } } diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index fa3ad2ae2b9..6047cc3d27a 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -28,7 +28,8 @@ import { } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests"; import { LoginEmailService } from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts index 0557227938f..b54529f6a2c 100644 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts +++ b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts @@ -15,6 +15,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ToastService } from "../../../../components/src/toast"; import { InputPasswordComponent } from "../input-password/input-password.component"; import { PasswordInputResult } from "../input-password/password-input-result"; diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index aad0df4e397..4583332cb88 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -12,6 +12,7 @@ import { TrustedDeviceUserDecryptionOption, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, + LoginSuccessHandlerService, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; @@ -35,7 +36,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { AsyncActionsModule, ButtonModule, @@ -117,7 +117,7 @@ export class SsoComponent implements OnInit { private accountService: AccountService, private toastService: ToastService, private ssoComponentService: SsoComponentService, - private syncService: SyncService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) { environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html"; @@ -378,8 +378,7 @@ export class SsoComponent implements OnInit { // Everything after the 2FA check is considered a successful login // Just have to figure out where to send the user - - await this.syncService.fullSync(true); + await this.loginSuccessHandlerService.run(authResult.userId); // Save off the OrgSsoIdentifier for use in the TDE flows (or elsewhere) // - TDE login decryption options component @@ -481,16 +480,7 @@ export class SsoComponent implements OnInit { } private async handleChangePasswordRequired(orgIdentifier: string) { - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - let route = "set-password"; - if (emailVerification) { - route = "set-password-jit"; - } - - await this.router.navigate([route], { + await this.router.navigate(["set-password-jit"], { queryParams: { identifier: orgIdentifier, }, diff --git a/libs/auth/src/common/models/domain/user-decryption-options.ts b/libs/auth/src/common/models/domain/user-decryption-options.ts index 95efe2b0077..00b78064d83 100644 --- a/libs/auth/src/common/models/domain/user-decryption-options.ts +++ b/libs/auth/src/common/models/domain/user-decryption-options.ts @@ -4,6 +4,8 @@ import { Jsonify } from "type-fest"; import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response"; import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response"; /** diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index d1cedebcf36..44f6afa5d23 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -4,6 +4,5 @@ export * from "./login-strategies/login-strategy.service"; export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; export * from "./auth-request/auth-request-api.service"; -export * from "./register-route.service"; export * from "./accounts/lock.service"; export * from "./login-success-handler/default-login-success-handler.service"; diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts index feb0cba1bdc..aa13afd5004 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -6,6 +6,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { GlobalState, KeyDefinition, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 1f98a117c88..57a653b205e 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -35,6 +35,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; diff --git a/libs/auth/src/common/services/register-route.service.ts b/libs/auth/src/common/services/register-route.service.ts deleted file mode 100644 index 5bc09db699e..00000000000 --- a/libs/auth/src/common/services/register-route.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Observable, map } from "rxjs"; - -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -// This is a temporary service to determine the correct route to use for registration based on the email verification feature flag. -export class RegisterRouteService { - constructor(private configService: ConfigService) {} - - registerRoute$(): Observable { - return this.configService.getFeatureFlag$(FeatureFlag.EmailVerification).pipe( - map((emailVerificationEnabled) => { - if (emailVerificationEnabled) { - return "/signup"; - } else { - return "/register"; - } - }), - ); - } -} diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts index ffb660e6a7f..7c44a6f1682 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts @@ -8,6 +8,8 @@ import { USER_DECRYPTION_OPTIONS_DISK, UserKeyDefinition, } from "@bitwarden/common/platform/state"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserId } from "@bitwarden/common/src/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; diff --git a/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts b/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts index 717e80b110d..24b3adacc21 100644 --- a/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts +++ b/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts @@ -18,6 +18,8 @@ export function decodeJwtTokenToJson(jwtToken: string): any { try { // Attempt to decode from URL-safe Base64 to UTF-8 decodedPayloadJSON = Utils.fromUrlB64ToUtf8(encodedPayload); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (decodingError) { throw new Error("Cannot decode the token"); } @@ -26,6 +28,8 @@ export function decodeJwtTokenToJson(jwtToken: string): any { // Attempt to parse the JSON payload const decodedToken = JSON.parse(decodedPayloadJSON); return decodedToken; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (jsonError) { throw new Error("Cannot parse the token's payload into JSON"); } diff --git a/libs/auth/tsconfig.json b/libs/auth/tsconfig.json index 6004a56fb55..8d08522ffce 100644 --- a/libs/auth/tsconfig.json +++ b/libs/auth/tsconfig.json @@ -1,5 +1,23 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "resolveJsonModule": true, + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/angular": ["../auth/src/angular"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/billing/jest.config.js b/libs/billing/jest.config.js index d9bae9633ea..c43606191b9 100644 --- a/libs/billing/jest.config.js +++ b/libs/billing/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/billing/tsconfig.json b/libs/billing/tsconfig.json index 6004a56fb55..bb08eb89d1c 100644 --- a/libs/billing/tsconfig.json +++ b/libs/billing/tsconfig.json @@ -1,5 +1,6 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": {}, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/common/jest.config.js b/libs/common/jest.config.js index d7f78abbf38..7e6c0997b9c 100644 --- a/libs/common/jest.config.js +++ b/libs/common/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../shared/jest.config.ts"); diff --git a/libs/common/spec/matchers/index.ts b/libs/common/spec/matchers/index.ts index 44440be5b54..b2e09cc8e92 100644 --- a/libs/common/spec/matchers/index.ts +++ b/libs/common/spec/matchers/index.ts @@ -1,16 +1,12 @@ -import * as matchers from "jest-extended"; - import { toBeFulfilled, toBeResolved, toBeRejected } from "./promise-fulfilled"; import { toAlmostEqual } from "./to-almost-equal"; +import { toContainPartialObjects } from "./to-contain-partial-objects"; import { toEqualBuffer } from "./to-equal-buffer"; export * from "./to-equal-buffer"; export * from "./to-almost-equal"; export * from "./promise-fulfilled"; -// add all jest-extended matchers -expect.extend(matchers); - export function addCustomMatchers() { expect.extend({ toEqualBuffer: toEqualBuffer, @@ -18,6 +14,7 @@ export function addCustomMatchers() { toBeFulfilled: toBeFulfilled, toBeResolved: toBeResolved, toBeRejected: toBeRejected, + toContainPartialObjects, }); } @@ -59,4 +56,9 @@ export interface CustomMatchers { * @returns CustomMatcherResult indicating whether or not the test passed */ toBeRejected(withinMs?: number): Promise; + /** + * Matches if the received array contains all the expected objects using partial matching (expect.objectContaining). + * @param expected An array of partial objects that should be contained in the received array. + */ + toContainPartialObjects(expected: Array): R; } diff --git a/libs/common/spec/matchers/to-contain-partial-objects.spec.ts b/libs/common/spec/matchers/to-contain-partial-objects.spec.ts new file mode 100644 index 00000000000..ab6f90adf17 --- /dev/null +++ b/libs/common/spec/matchers/to-contain-partial-objects.spec.ts @@ -0,0 +1,77 @@ +describe("toContainPartialObjects", () => { + describe("matches", () => { + it("if the array only contains the partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 1 }, { id: 2 }]; + + expect(actual).toContainPartialObjects(expected); + }); + + it("if the array contains the partial objects and other objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + { + id: 3, + name: "baz", + }, + ]; + + const expected = [{ id: 1 }, { id: 2 }]; + + expect(actual).toContainPartialObjects(expected); + }); + }); + + describe("doesn't match", () => { + it("if the array does not contain any partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 1, name: "Foo" }]; + + expect(actual).not.toContainPartialObjects(expected); + }); + + it("if the array contains some but not all partial objects", () => { + const actual = [ + { + id: 1, + name: "foo", + }, + { + id: 2, + name: "bar", + }, + ]; + + const expected = [{ id: 2 }, { id: 3 }]; + + expect(actual).not.toContainPartialObjects(expected); + }); + }); +}); diff --git a/libs/common/spec/matchers/to-contain-partial-objects.ts b/libs/common/spec/matchers/to-contain-partial-objects.ts new file mode 100644 index 00000000000..f072ca6fba6 --- /dev/null +++ b/libs/common/spec/matchers/to-contain-partial-objects.ts @@ -0,0 +1,31 @@ +import { EOL } from "os"; + +import { diff } from "jest-diff"; + +export const toContainPartialObjects: jest.CustomMatcher = function ( + received: Array, + expected: Array, +) { + const matched = this.equals( + received, + expect.arrayContaining(expected.map((e) => expect.objectContaining(e))), + ); + + if (matched) { + return { + message: () => + "Expected the received array NOT to include partial matches for all expected objects." + + EOL + + diff(expected, received), + pass: true, + }; + } + + return { + message: () => + "Expected the received array to contain partial matches for all expected objects." + + EOL + + diff(expected, received), + pass: false, + }; +}; diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 997974e0581..daccc4bd16e 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { - CollectionRequest, CollectionAccessDetailsResponse, CollectionDetailsResponse, + CollectionRequest, CollectionResponse, } from "@bitwarden/admin-console/common"; @@ -95,7 +95,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; -import { TaxRateResponse } from "../billing/models/response/tax-rate.response"; import { DeleteRecoverRequest } from "../models/request/delete-recover.request"; import { EventRequest } from "../models/request/event.request"; import { KdfRequest } from "../models/request/kdf.request"; @@ -137,7 +136,7 @@ import { OptionalCipherResponse } from "../vault/models/response/optional-cipher */ export abstract class ApiService { send: ( - method: "GET" | "POST" | "PUT" | "DELETE", + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, authed: boolean, @@ -376,7 +375,6 @@ export abstract class ApiService { ): Promise>; deleteOrganizationConnection: (id: string) => Promise; getPlans: () => Promise>; - getTaxRates: () => Promise>; getProviderUsers: (providerId: string) => Promise>; getProviderUser: (providerId: string, id: string) => Promise; diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts index 5ecfca5ab84..5a393ed1996 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts @@ -1,7 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ListResponse } from "@bitwarden/common/models/response/list.response"; - +import { ListResponse } from "../../../models/response/list.response"; import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request"; import { OrganizationDomainSsoDetailsResponse } from "./responses/organization-domain-sso-details.response"; diff --git a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts index c4817306a63..066d2d381e7 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../../models/response/base.response"; export class VerifiedOrganizationDomainSsoDetailsResponse extends BaseResponse { organizationName: string; diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 9fd3b358568..000d1655416 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -1,7 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; - import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; import { OrganizationSsoRequest } from "../../../auth/models/request/organization-sso.request"; import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; @@ -13,6 +11,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; +import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; @@ -53,11 +52,11 @@ export class OrganizationApiServiceAbstraction { updatePasswordManagerSeats: ( id: string, request: OrganizationSubscriptionUpdateRequest, - ) => Promise; + ) => Promise; updateSecretsManagerSubscription: ( id: string, request: OrganizationSmSubscriptionUpdateRequest, - ) => Promise; + ) => Promise; updateSeats: (id: string, request: SeatRequest) => Promise; updateStorage: (id: string, request: StorageRequest) => Promise; verifyBank: (id: string, request: VerifyBankRequest) => Promise; diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index 2161feb516e..da81f340fda 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -17,7 +17,7 @@ export function canAccessSettingsTab(org: Organization): boolean { org.canManageSso || org.canManageScim || org.canAccessImport || - org.canAccessExport(false) || // Feature flag value doesn't matter here, providers will have access to this group anyway + org.canAccessExport || org.canManageDeviceApprovals ); } diff --git a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts b/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts index b5c0f6291fc..c25a153a068 100644 --- a/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts +++ b/libs/common/src/admin-console/abstractions/organization/vnext.organization.service.ts @@ -17,7 +17,7 @@ export function canAccessSettingsTab(org: Organization): boolean { org.canManageSso || org.canManageScim || org.canAccessImport || - org.canAccessExport(false) || // Feature flag value doesn't matter here, providers will have access to this group anyway + org.canAccessExport || org.canManageDeviceApprovals ); } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 8441298bbff..9dcc9f0752c 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -182,11 +182,7 @@ export class Organization { ); } - canAccessExport(removeProviderExport: boolean) { - if (!removeProviderExport && this.isProviderUser) { - return true; - } - + get canAccessExport() { return ( this.isMember && (this.type === OrganizationUserType.Owner || diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts index 7497a77e6f2..1052fe504d4 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts @@ -1,14 +1,13 @@ import { mock } from "jest-mock-extended"; import { lastValueFrom } from "rxjs"; -import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; - import { ApiService } from "../../../abstractions/api.service"; +import { ListResponse } from "../../../models/response/list.response"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { OrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/organization-domain-sso-details.response"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; import { OrgDomainApiService } from "./org-domain-api.service"; import { OrgDomainService } from "./org-domain.service"; diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index d62fd49a6a4..598bb2a29db 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; import { ApiService } from "../../../abstractions/api.service"; import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; @@ -14,6 +13,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; +import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; @@ -161,27 +161,29 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction async updatePasswordManagerSeats( id: string, request: OrganizationSubscriptionUpdateRequest, - ): Promise { - return this.apiService.send( + ): Promise { + const r = await this.apiService.send( "POST", "/organizations/" + id + "/subscription", request, true, - false, + true, ); + return new ProfileOrganizationResponse(r); } async updateSecretsManagerSubscription( id: string, request: OrganizationSmSubscriptionUpdateRequest, - ): Promise { - return this.apiService.send( + ): Promise { + const r = await this.apiService.send( "POST", "/organizations/" + id + "/sm-subscription", request, true, - false, + true, ); + return new ProfileOrganizationResponse(r); } async updateSeats(id: string, request: SeatRequest): Promise { diff --git a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts b/libs/common/src/admin-console/services/organization/vnext-organization.state.ts index 48e09d6d076..fea0423f389 100644 --- a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts +++ b/libs/common/src/admin-console/services/organization/vnext-organization.state.ts @@ -1,7 +1,6 @@ import { Jsonify } from "type-fest"; -import { ORGANIZATIONS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; - +import { ORGANIZATIONS_DISK, UserKeyDefinition } from "../../../platform/state"; import { OrganizationData } from "../../models/data/organization.data"; /** diff --git a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts index 0af89928449..92f0ebf1667 100644 --- a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts @@ -36,4 +36,10 @@ export abstract class DevicesApiServiceAbstraction { * @param deviceIdentifier - current device identifier */ postDeviceTrustLoss: (deviceIdentifier: string) => Promise; + + /** + * Deactivates a device + * @param deviceId - The device ID + */ + deactivateDevice: (deviceId: string) => Promise; } diff --git a/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts b/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts index a02ccc64876..ba6890947c1 100644 --- a/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts @@ -1,17 +1,18 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; +import { DeviceResponse } from "./responses/device.response"; import { DeviceView } from "./views/device.view"; export abstract class DevicesServiceAbstraction { - getDevices$: () => Observable>; - getDeviceByIdentifier$: (deviceIdentifier: string) => Observable; - isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable; - updateTrustedDeviceKeys$: ( + abstract getDevices$(): Observable>; + abstract getDeviceByIdentifier$(deviceIdentifier: string): Observable; + abstract isDeviceKnownForUser$(email: string, deviceIdentifier: string): Observable; + abstract updateTrustedDeviceKeys$( deviceIdentifier: string, devicePublicKeyEncryptedUserKey: string, userKeyEncryptedDevicePublicKey: string, deviceKeyEncryptedDevicePrivateKey: string, - ) => Observable; + ): Observable; + abstract deactivateDevice$(deviceId: string): Observable; + abstract getCurrentDevice$(): Observable; } diff --git a/libs/common/src/auth/abstractions/devices/responses/device.response.ts b/libs/common/src/auth/abstractions/devices/responses/device.response.ts index a4e40037b05..84a2fb03c28 100644 --- a/libs/common/src/auth/abstractions/devices/responses/device.response.ts +++ b/libs/common/src/auth/abstractions/devices/responses/device.response.ts @@ -1,6 +1,11 @@ import { DeviceType } from "../../../../enums"; import { BaseResponse } from "../../../../models/response/base.response"; +export interface DevicePendingAuthRequest { + id: string; + creationDate: string; +} + export class DeviceResponse extends BaseResponse { id: string; userId: string; @@ -9,6 +14,9 @@ export class DeviceResponse extends BaseResponse { type: DeviceType; creationDate: string; revisionDate: string; + isTrusted: boolean; + devicePendingAuthRequest: DevicePendingAuthRequest | null; + constructor(response: any) { super(response); this.id = this.getResponseProperty("Id"); @@ -18,5 +26,7 @@ export class DeviceResponse extends BaseResponse { this.type = this.getResponseProperty("Type"); this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); + this.isTrusted = this.getResponseProperty("IsTrusted"); + this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest"); } } diff --git a/libs/common/src/auth/abstractions/devices/views/device.view.ts b/libs/common/src/auth/abstractions/devices/views/device.view.ts index a901eb998b4..22e522b9eb0 100644 --- a/libs/common/src/auth/abstractions/devices/views/device.view.ts +++ b/libs/common/src/auth/abstractions/devices/views/device.view.ts @@ -12,6 +12,7 @@ export class DeviceView implements View { type: DeviceType; creationDate: string; revisionDate: string; + response: DeviceResponse; constructor(deviceResponse: DeviceResponse) { Object.assign(this, deviceResponse); diff --git a/libs/common/src/auth/models/request/update-devices-trust.request.ts b/libs/common/src/auth/models/request/update-devices-trust.request.ts index 8e3ce86c1a9..21fe0f600dc 100644 --- a/libs/common/src/auth/models/request/update-devices-trust.request.ts +++ b/libs/common/src/auth/models/request/update-devices-trust.request.ts @@ -8,8 +8,8 @@ export class UpdateDevicesTrustRequest extends SecretVerificationRequest { } export class DeviceKeysUpdateRequest { - encryptedPublicKey: string; - encryptedUserKey: string; + encryptedPublicKey: string | undefined; + encryptedUserKey: string | undefined; } export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest { diff --git a/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts b/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts index 791c4688078..84103fe5d29 100644 --- a/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts +++ b/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts @@ -1,8 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { RotateableKeySet } from "../../../../../auth/src/common/models"; +import { EncString } from "../../../platform/models/domain/enc-string"; export class WebauthnRotateCredentialRequest { id: string; diff --git a/libs/common/src/auth/services/anonymous-hub.service.ts b/libs/common/src/auth/services/anonymous-hub.service.ts index a268c8a2712..3900dd53ee0 100644 --- a/libs/common/src/auth/services/anonymous-hub.service.ts +++ b/libs/common/src/auth/services/anonymous-hub.service.ts @@ -9,6 +9,8 @@ import { import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; import { firstValueFrom } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AuthRequestServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { NotificationType } from "../../enums"; import { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 5663384714d..4b7b8a2b262 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index bab83fc55db..d855a1be34f 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -11,6 +11,8 @@ import { switchMap, } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { StateService } from "../../platform/abstractions/state.service"; diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index 409150552ad..a94c8b6e422 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -4,6 +4,8 @@ import { firstValueFrom, map, Observable } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; @@ -335,6 +337,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); return new SymmetricCryptoKey(userKey) as UserKey; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // If either decryption effort fails, we want to remove the device key this.logService.error("Failed to decrypt using device key. Removing device key."); diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 66a91a693e5..943653e3129 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -3,7 +3,11 @@ import { BehaviorSubject, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState } from "../../../spec/fake-state"; diff --git a/libs/common/src/auth/services/devices-api.service.implementation.spec.ts b/libs/common/src/auth/services/devices-api.service.implementation.spec.ts new file mode 100644 index 00000000000..7aea36c7bd4 --- /dev/null +++ b/libs/common/src/auth/services/devices-api.service.implementation.spec.ts @@ -0,0 +1,100 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { ApiService } from "../../abstractions/api.service"; +import { DeviceResponse } from "../abstractions/devices/responses/device.response"; + +import { DevicesApiServiceImplementation } from "./devices-api.service.implementation"; + +describe("DevicesApiServiceImplementation", () => { + let devicesApiService: DevicesApiServiceImplementation; + let apiService: MockProxy; + + beforeEach(() => { + apiService = mock(); + devicesApiService = new DevicesApiServiceImplementation(apiService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("getKnownDevice", () => { + it("calls api with correct parameters", async () => { + const email = "test@example.com"; + const deviceIdentifier = "device123"; + apiService.send.mockResolvedValue(true); + + const result = await devicesApiService.getKnownDevice(email, deviceIdentifier); + + expect(result).toBe(true); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + "/devices/knowndevice", + null, + false, + true, + null, + expect.any(Function), + ); + }); + }); + + describe("getDeviceByIdentifier", () => { + it("returns device response", async () => { + const deviceIdentifier = "device123"; + const mockResponse = { id: "123", name: "Test Device" }; + apiService.send.mockResolvedValue(mockResponse); + + const result = await devicesApiService.getDeviceByIdentifier(deviceIdentifier); + + expect(result).toBeInstanceOf(DeviceResponse); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + `/devices/identifier/${deviceIdentifier}`, + null, + true, + true, + ); + }); + }); + + describe("updateTrustedDeviceKeys", () => { + it("updates device keys and returns device response", async () => { + const deviceIdentifier = "device123"; + const publicKeyEncrypted = "encryptedPublicKey"; + const userKeyEncrypted = "encryptedUserKey"; + const deviceKeyEncrypted = "encryptedDeviceKey"; + const mockResponse = { id: "123", name: "Test Device" }; + apiService.send.mockResolvedValue(mockResponse); + + const result = await devicesApiService.updateTrustedDeviceKeys( + deviceIdentifier, + publicKeyEncrypted, + userKeyEncrypted, + deviceKeyEncrypted, + ); + + expect(result).toBeInstanceOf(DeviceResponse); + expect(apiService.send).toHaveBeenCalledWith( + "PUT", + `/devices/${deviceIdentifier}/keys`, + { + encryptedPrivateKey: deviceKeyEncrypted, + encryptedPublicKey: userKeyEncrypted, + encryptedUserKey: publicKeyEncrypted, + }, + true, + true, + ); + }); + }); + + describe("error handling", () => { + it("propagates api errors", async () => { + const error = new Error("API Error"); + apiService.send.mockRejectedValue(error); + + await expect(devicesApiService.getDevices()).rejects.toThrow("API Error"); + }); + }); +}); diff --git a/libs/common/src/auth/services/devices-api.service.implementation.ts b/libs/common/src/auth/services/devices-api.service.implementation.ts index 711f0bb68ec..cf760effbdf 100644 --- a/libs/common/src/auth/services/devices-api.service.implementation.ts +++ b/libs/common/src/auth/services/devices-api.service.implementation.ts @@ -117,4 +117,8 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac }, ); } + + async deactivateDevice(deviceId: string): Promise { + await this.apiService.send("POST", `/devices/${deviceId}/deactivate`, null, true, false); + } } diff --git a/libs/common/src/auth/services/devices/devices.service.implementation.ts b/libs/common/src/auth/services/devices/devices.service.implementation.ts index 6032ed66a89..cdaa7a9fc4e 100644 --- a/libs/common/src/auth/services/devices/devices.service.implementation.ts +++ b/libs/common/src/auth/services/devices/devices.service.implementation.ts @@ -1,6 +1,7 @@ import { Observable, defer, map } from "rxjs"; import { ListResponse } from "../../../models/response/list.response"; +import { AppIdService } from "../../../platform/abstractions/app-id.service"; import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction"; import { DeviceResponse } from "../../abstractions/devices/responses/device.response"; import { DeviceView } from "../../abstractions/devices/views/device.view"; @@ -15,7 +16,10 @@ import { DevicesApiServiceAbstraction } from "../../abstractions/devices-api.ser * (i.e., promsise --> observables are cold until subscribed to) */ export class DevicesServiceImplementation implements DevicesServiceAbstraction { - constructor(private devicesApiService: DevicesApiServiceAbstraction) {} + constructor( + private devicesApiService: DevicesApiServiceAbstraction, + private appIdService: AppIdService, + ) {} /** * @description Gets the list of all devices. @@ -65,4 +69,21 @@ export class DevicesServiceImplementation implements DevicesServiceAbstraction { ), ).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse))); } + + /** + * @description Deactivates a device + */ + deactivateDevice$(deviceId: string): Observable { + return defer(() => this.devicesApiService.deactivateDevice(deviceId)); + } + + /** + * @description Gets the current device. + */ + getCurrentDevice$(): Observable { + return defer(async () => { + const deviceIdentifier = await this.appIdService.getAppId(); + return this.devicesApiService.getDeviceByIdentifier(deviceIdentifier); + }); + } } diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index b1bf87693c1..660f1124f4a 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts index ea6e1045c10..14e7522a836 100644 --- a/libs/common/src/auth/services/master-password/master-password.service.ts +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable } from "rxjs"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; - import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; +import { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { EncryptionType } from "../../../platform/enums"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; @@ -180,10 +179,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr let decUserKey: Uint8Array; if (userKey.encryptionType === EncryptionType.AesCbc256_B64) { - decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey); + decUserKey = await this.encryptService.decryptToBytes( + userKey, + masterKey, + "Content: User Key; Encrypting Key: Master Key", + ); } else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { const newKey = await this.keyGenerationService.stretchKey(masterKey); - decUserKey = await this.encryptService.decryptToBytes(userKey, newKey); + decUserKey = await this.encryptService.decryptToBytes( + userKey, + newKey, + "Content: User Key; Encrypting Key: Stretched Master Key", + ); } else { throw new Error("Unsupported encryption type."); } diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index ed622b21c86..ddd24ae7907 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -2,13 +2,15 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { UserId } from "../../../../common/src/types/guid"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; +import { UserId } from "../../types/guid"; import { Account, AccountInfo, AccountService } from "../abstractions/account.service"; import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index c5451ce30c6..22d5384e6ac 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -6,10 +6,12 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { Utils } from "../../platform/misc/utils"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index 4aa3a632855..102e4bac8da 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -7,14 +7,19 @@ import { UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { KdfConfig, KeyService } from "@bitwarden/key-management"; +import { + BiometricsService, + BiometricsStatus, + KdfConfig, + KeyService, +} from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { LogService } from "../../../platform/abstractions/log.service"; -import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { HashPurpose } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { UserId } from "../../../types/guid"; @@ -36,10 +41,9 @@ describe("UserVerificationService", () => { const userVerificationApiService = mock(); const userDecryptionOptionsService = mock(); const pinService = mock(); - const logService = mock(); const vaultTimeoutSettingsService = mock(); - const platformUtilsService = mock(); const kdfConfigService = mock(); + const biometricsService = mock(); const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -56,10 +60,8 @@ describe("UserVerificationService", () => { userVerificationApiService, userDecryptionOptionsService, pinService, - logService, - vaultTimeoutSettingsService, - platformUtilsService, kdfConfigService, + biometricsService, ); }); @@ -113,26 +115,15 @@ describe("UserVerificationService", () => { ); test.each([ - [true, true, true, true], - [true, true, true, false], - [true, true, false, false], - [false, true, false, true], - [false, false, false, false], - [false, false, true, false], - [false, false, false, true], + [true, BiometricsStatus.Available], + [false, BiometricsStatus.DesktopDisconnected], + [false, BiometricsStatus.HardwareUnavailable], ])( "returns %s for biometrics availability when isBiometricLockSet is %s, hasUserKeyStored is %s, and supportsSecureStorage is %s", - async ( - expectedReturn: boolean, - isBiometricsLockSet: boolean, - isBiometricsUserKeyStored: boolean, - platformSupportSecureStorage: boolean, - ) => { + async (expectedReturn: boolean, biometricsStatus: BiometricsStatus) => { setMasterPasswordAvailability(false); setPinAvailability("DISABLED"); - vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet); - keyService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored); - platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage); + biometricsService.getBiometricsStatus.mockResolvedValue(biometricsStatus); const result = await sut.getAvailableVerificationOptions("client"); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 822ee70ec5b..4735da32b6b 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -3,17 +3,19 @@ import { firstValueFrom, map } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { KdfConfigService, KeyService } from "@bitwarden/key-management"; +import { + BiometricsService, + BiometricsStatus, + KdfConfigService, + KeyService, +} from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction"; -import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { LogService } from "../../../platform/abstractions/log.service"; -import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { HashPurpose } from "../../../platform/enums"; -import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum"; import { UserId } from "../../../types/guid"; -import { UserKey } from "../../../types/key"; import { AccountService } from "../../abstractions/account.service"; import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; @@ -47,10 +49,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private userVerificationApiService: UserVerificationApiServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private pinService: PinServiceAbstraction, - private logService: LogService, - private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, - private platformUtilsService: PlatformUtilsService, private kdfConfigService: KdfConfigService, + private biometricsService: BiometricsService, ) {} async getAvailableVerificationOptions( @@ -58,17 +58,13 @@ export class UserVerificationService implements UserVerificationServiceAbstracti ): Promise { const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (verificationType === "client") { - const [ - userHasMasterPassword, - isPinDecryptionAvailable, - biometricsLockSet, - biometricsUserKeyStored, - ] = await Promise.all([ - this.hasMasterPasswordAndMasterKeyHash(userId), - this.pinService.isPinDecryptionAvailable(userId), - this.vaultTimeoutSettingsService.isBiometricLockSet(userId), - this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric, userId), - ]); + const [userHasMasterPassword, isPinDecryptionAvailable, biometricsStatus] = await Promise.all( + [ + this.hasMasterPasswordAndMasterKeyHash(userId), + this.pinService.isPinDecryptionAvailable(userId), + this.biometricsService.getBiometricsStatus(), + ], + ); // note: we do not need to check this.platformUtilsService.supportsBiometric() because // we can just use the logic below which works for both desktop & the browser extension. @@ -77,9 +73,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti client: { masterPassword: userHasMasterPassword, pin: isPinDecryptionAvailable, - biometrics: - biometricsLockSet && - (biometricsUserKeyStored || !this.platformUtilsService.supportsSecureStorage()), + biometrics: biometricsStatus === BiometricsStatus.Available, }, server: { masterPassword: false, @@ -169,6 +163,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti const request = new VerifyOTPRequest(verification.secret); try { await this.userVerificationApiService.postAccountVerifyOTP(request); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(this.i18nService.t("invalidVerificationCode")); } @@ -227,6 +223,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti request.masterPasswordHash = serverKeyHash; try { policyOptions = await this.userVerificationApiService.postAccountVerifyPassword(request); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(this.i18nService.t("invalidMasterPassword")); } @@ -253,17 +251,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } private async verifyUserByBiometrics(): Promise { - let userKey: UserKey; - // Biometrics crashes and doesn't return a value if the user cancels the prompt - try { - userKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Biometric); - } catch (e) { - this.logService.error(`Biometrics User Verification failed: ${e.message}`); - // So, any failures should be treated as a failed verification - return false; - } - - return userKey != null; + return this.biometricsService.authenticateWithBiometrics(); } async requestOTP() { diff --git a/libs/common/src/autofill/services/domain-settings.service.spec.ts b/libs/common/src/autofill/services/domain-settings.service.spec.ts index 24e3763eb45..36f7d0eacec 100644 --- a/libs/common/src/autofill/services/domain-settings.service.spec.ts +++ b/libs/common/src/autofill/services/domain-settings.service.spec.ts @@ -1,6 +1,8 @@ +import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; @@ -8,6 +10,7 @@ import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-se describe("DefaultDomainSettingsService", () => { let domainSettingsService: DomainSettingsService; + let configService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); @@ -19,10 +22,13 @@ describe("DefaultDomainSettingsService", () => { ]; beforeEach(() => { - domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + configService = mock(); + configService.getFeatureFlag$.mockImplementation(() => of(false)); + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); jest.spyOn(domainSettingsService, "getUrlEquivalentDomains"); domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); + domainSettingsService.blockedInteractionsUris$ = of({}); }); describe("getUrlEquivalentDomains", () => { diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index 708341563e0..b2833b9ee25 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -1,13 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { map, Observable } from "rxjs"; +import { map, Observable, switchMap, of } from "rxjs"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; import { NeverDomains, EquivalentDomains, UriMatchStrategySetting, UriMatchStrategy, } from "../../models/domain/domain-service"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { Utils } from "../../platform/misc/utils"; import { DOMAIN_SETTINGS_DISK, @@ -23,10 +25,20 @@ const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", { deserializer: (value: boolean) => value ?? true, }); +// Domain exclusion list for notifications const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", { deserializer: (value: NeverDomains) => value ?? null, }); +// Domain exclusion list for content script injections +const BLOCKED_INTERACTIONS_URIS = new KeyDefinition( + DOMAIN_SETTINGS_DISK, + "blockedInteractionsUris", + { + deserializer: (value: NeverDomains) => value ?? {}, + }, +); + const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", { deserializer: (value: EquivalentDomains) => value ?? null, clearOn: ["logout"], @@ -41,15 +53,45 @@ const DEFAULT_URI_MATCH_STRATEGY = new UserKeyDefinition( }, ); +/** + * The Domain Settings service; provides client settings state for "active client view" URI concerns + */ export abstract class DomainSettingsService { + /** + * Indicates if the favicons for ciphers' URIs should be shown instead of a placeholder + */ showFavicons$: Observable; setShowFavicons: (newValue: boolean) => Promise; + + /** + * User-specified URIs for which the client notifications should not appear + */ neverDomains$: Observable; setNeverDomains: (newValue: NeverDomains) => Promise; + + /** + * User-specified URIs for which client content script injections should not occur, and the state + * of banner/notice visibility for those domains within the client + */ + blockedInteractionsUris$: Observable; + setBlockedInteractionsUris: (newValue: NeverDomains) => Promise; + + /** + * URIs which should be treated as equivalent to each other for various concerns (autofill, etc) + */ equivalentDomains$: Observable; setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise; + + /** + * User-specified default for URI-matching strategies (for example, when determining relevant + * ciphers for an active browser tab). Can be overridden by cipher-specific settings. + */ defaultUriMatchStrategy$: Observable; setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise; + + /** + * Helper function for the common resolution of a given URL against equivalent domains + */ getUrlEquivalentDomains: (url: string) => Observable>; } @@ -60,19 +102,37 @@ export class DefaultDomainSettingsService implements DomainSettingsService { private neverDomainsState: GlobalState; readonly neverDomains$: Observable; + private blockedInteractionsUrisState: GlobalState; + readonly blockedInteractionsUris$: Observable; + private equivalentDomainsState: ActiveUserState; readonly equivalentDomains$: Observable; private defaultUriMatchStrategyState: ActiveUserState; readonly defaultUriMatchStrategy$: Observable; - constructor(private stateProvider: StateProvider) { + constructor( + private stateProvider: StateProvider, + private configService: ConfigService, + ) { this.showFaviconsState = this.stateProvider.getGlobal(SHOW_FAVICONS); this.showFavicons$ = this.showFaviconsState.state$.pipe(map((x) => x ?? true)); this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS); this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null)); + // Needs to be global to prevent pre-login injections + this.blockedInteractionsUrisState = this.stateProvider.getGlobal(BLOCKED_INTERACTIONS_URIS); + + this.blockedInteractionsUris$ = this.configService + .getFeatureFlag$(FeatureFlag.BlockBrowserInjectionsByDomain) + .pipe( + switchMap((featureIsEnabled) => + featureIsEnabled ? this.blockedInteractionsUrisState.state$ : of({} as NeverDomains), + ), + map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : {})), + ); + this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS); this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null)); @@ -90,6 +150,10 @@ export class DefaultDomainSettingsService implements DomainSettingsService { await this.neverDomainsState.update(() => newValue); } + async setBlockedInteractionsUris(newValue: NeverDomains): Promise { + await this.blockedInteractionsUrisState.update(() => newValue); + } + async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise { await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue); } diff --git a/libs/common/src/autofill/utils.spec.ts b/libs/common/src/autofill/utils.spec.ts index b09dc723b8e..554dc973b48 100644 --- a/libs/common/src/autofill/utils.spec.ts +++ b/libs/common/src/autofill/utils.spec.ts @@ -1,9 +1,6 @@ -import { - normalizeExpiryYearFormat, - isCardExpired, - parseYearMonthExpiry, -} from "@bitwarden/common/autofill/utils"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CardView } from "../vault/models/view/card.view"; + +import { normalizeExpiryYearFormat, isCardExpired, parseYearMonthExpiry } from "./utils"; function getExpiryYearValueFormats(currentCentury: string) { return [ @@ -86,12 +83,14 @@ function getCardExpiryDateValues() { // `Date` months are zero-indexed, our expiry date month inputs are one-indexed const currentMonth = currentDate.getMonth() + 1; + const currentDateLastMonth = new Date(currentDate.setMonth(-1)); + return [ [null, null, false], // no month, no year [undefined, undefined, false], // no month, no year, invalid values ["", "", false], // no month, no year, invalid values ["12", "agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", false], // invalid values - ["0", `${currentYear}`, true], // invalid month + ["0", `${currentYear}`, false], // invalid month ["0", `${currentYear - 1}`, true], // invalid 0 month ["00", `${currentYear + 1}`, false], // invalid 0 month [`${currentMonth}`, "0000", true], // current month, in the year 2000 @@ -103,7 +102,7 @@ function getCardExpiryDateValues() { [`${currentMonth + 36}`, `${currentYear - 1}`, true], // even though the month value would put the date 3 years into the future when calculated with `Date`, an explicit year in the past indicates the card is expired [`${currentMonth}`, `${currentYear}`, false], // this year, this month (not expired until the month is over) [`${currentMonth}`, `${currentYear}`.slice(-2), false], // This month, this year (not expired until the month is over) - [`${currentMonth - 1}`, `${currentYear}`, true], // last month + [`${currentDateLastMonth.getMonth() + 1}`, `${currentDateLastMonth.getFullYear()}`, true], // last month [`${currentMonth - 1}`, `${currentYear + 1}`, false], // 11 months from now ]; } diff --git a/libs/common/src/autofill/utils.ts b/libs/common/src/autofill/utils.ts index 271952e27d1..a77ea8a715d 100644 --- a/libs/common/src/autofill/utils.ts +++ b/libs/common/src/autofill/utils.ts @@ -1,13 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +import { CardView } from "../vault/models/view/card.view"; + import { DelimiterPatternExpression, ExpiryFullYearPattern, ExpiryFullYearPatternExpression, IrrelevantExpiryCharactersPatternExpression, MonthPatternExpression, -} from "@bitwarden/common/autofill/constants"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +} from "./constants"; type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`; @@ -25,11 +24,11 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu let expirationYear = yearInputIsEmpty ? null : `${yearInput}`; // Exit early if year is already formatted correctly or empty - if (yearInputIsEmpty || /^[1-9]{1}\d{3}$/.test(expirationYear)) { + if (yearInputIsEmpty || (expirationYear && /^[1-9]{1}\d{3}$/.test(expirationYear))) { return expirationYear as Year; } - expirationYear = expirationYear + expirationYear = (expirationYear || "") // For safety, because even input[type="number"] will allow decimals .replace(/[^\d]/g, "") // remove any leading zero padding (leave the last leading zero if it ends the string) @@ -53,7 +52,7 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu /** * Takes a cipher card view and returns "true" if the month and year affirmativey indicate - * the card is expired. + * the card is expired. Uncertain cases return "false". * * @param {CardView} cipherCard * @return {*} {boolean} @@ -62,27 +61,38 @@ export function isCardExpired(cipherCard: CardView): boolean { if (cipherCard) { const { expMonth = null, expYear = null } = cipherCard; + if (!expYear) { + return false; + } + const now = new Date(); const normalizedYear = normalizeExpiryYearFormat(expYear); + const parsedYear = normalizedYear ? parseInt(normalizedYear, 10) : NaN; - // If the card year is before the current year, don't bother checking the month - if (normalizedYear && parseInt(normalizedYear, 10) < now.getFullYear()) { + const expiryYearIsBeforeCurrentYear = parsedYear < now.getFullYear(); + const expiryYearIsAfterCurrentYear = parsedYear > now.getFullYear(); + + // If the expiry year is before the current year, skip checking the month, since it must be expired + if (normalizedYear && expiryYearIsBeforeCurrentYear) { return true; } + // If the expiry year is after the current year, skip checking the month, since it cannot be expired + if (normalizedYear && expiryYearIsAfterCurrentYear) { + return false; + } + if (normalizedYear && expMonth) { const parsedMonthInteger = parseInt(expMonth, 10); + const parsedMonthIsValid = parsedMonthInteger && !isNaN(parsedMonthInteger); - const parsedMonth = isNaN(parsedMonthInteger) - ? 0 - : // Add a month floor of 0 to protect against an invalid low month value of "0" or negative integers - Math.max( - // `Date` months are zero-indexed - parsedMonthInteger - 1, - 0, - ); + // If the parsed month value is 0, we don't know when the expiry passes this year, so do not treat it as expired + if (!parsedMonthIsValid) { + return false; + } - const parsedYear = parseInt(normalizedYear, 10); + // `Date` months are zero-indexed + const parsedMonth = parsedMonthInteger - 1; // First day of the next month const cardExpiry = new Date(parsedYear, parsedMonth + 1, 1); @@ -250,13 +260,18 @@ function parseNonDelimitedYearMonthExpiry(dateInput: string): [string | null, st parsedMonth = dateInput.slice(-1); const currentYear = new Date().getFullYear(); - const normalizedParsedYear = parseInt(normalizeExpiryYearFormat(parsedYear), 10); - const normalizedParsedYearAlternative = parseInt( - normalizeExpiryYearFormat(dateInput.slice(-2)), - 10, - ); + const normalizedYearFormat = normalizeExpiryYearFormat(parsedYear); + const normalizedParsedYear = normalizedYearFormat && parseInt(normalizedYearFormat, 10); + const normalizedExpiryYearFormat = normalizeExpiryYearFormat(dateInput.slice(-2)); + const normalizedParsedYearAlternative = + normalizedExpiryYearFormat && parseInt(normalizedExpiryYearFormat, 10); - if (normalizedParsedYear < currentYear && normalizedParsedYearAlternative >= currentYear) { + if ( + normalizedParsedYear && + normalizedParsedYear < currentYear && + normalizedParsedYearAlternative && + normalizedParsedYearAlternative >= currentYear + ) { parsedYear = dateInput.slice(-2); parsedMonth = dateInput.slice(0, 1); } @@ -288,17 +303,24 @@ export function parseYearMonthExpiry(combinedExpiryValue: string): [Year | null, // If there is only one date part, no delimiter was found in the passed value if (dateParts.length === 1) { - [parsedYear, parsedMonth] = parseNonDelimitedYearMonthExpiry(sanitizedFirstPart); + const [parsedNonDelimitedYear, parsedNonDelimitedMonth] = + parseNonDelimitedYearMonthExpiry(sanitizedFirstPart); + + parsedYear = parsedNonDelimitedYear; + parsedMonth = parsedNonDelimitedMonth; } // There are multiple date parts else { - [parsedYear, parsedMonth] = parseDelimitedYearMonthExpiry([ + const [parsedDelimitedYear, parsedDelimitedMonth] = parseDelimitedYearMonthExpiry([ sanitizedFirstPart, sanitizedSecondPart, ]); + + parsedYear = parsedDelimitedYear; + parsedMonth = parsedDelimitedMonth; } - const normalizedParsedYear = normalizeExpiryYearFormat(parsedYear); + const normalizedParsedYear = parsedYear ? normalizeExpiryYearFormat(parsedYear) : null; const normalizedParsedMonth = parsedMonth?.replace(/^0+/, "").slice(0, 2); // Set "empty" values to null diff --git a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts index 06400081e11..e0e8b7377c5 100644 --- a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts @@ -3,7 +3,7 @@ import { BillingInvoiceResponse, BillingTransactionResponse, -} from "@bitwarden/common/billing/models/response/billing.response"; +} from "../../models/response/billing.response"; export class AccountBillingApiServiceAbstraction { getBillingInvoices: (status?: string, startAfter?: string) => Promise; diff --git a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts index 8fbbc7c1c91..a4253226880 100644 --- a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts @@ -11,27 +11,32 @@ export type BillingAccountProfile = { export abstract class BillingAccountProfileStateService { /** - * Emits `true` when the active user's account has been granted premium from any of the + * Emits `true` when the user's account has been granted premium from any of the * organizations it is a member of. Otherwise, emits `false` */ - hasPremiumFromAnyOrganization$: Observable; + abstract hasPremiumFromAnyOrganization$(userId: UserId): Observable; /** - * Emits `true` when the active user's account has an active premium subscription at the + * Emits `true` when the user's account has an active premium subscription at the * individual user level */ - hasPremiumPersonally$: Observable; + abstract hasPremiumPersonally$(userId: UserId): Observable; /** * Emits `true` when either `hasPremiumPersonally` or `hasPremiumFromAnyOrganization` is `true` */ - hasPremiumFromAnySource$: Observable; + abstract hasPremiumFromAnySource$(userId: UserId): Observable; /** - * Sets the active user's premium status fields upon every full sync, either from their personal + * Emits `true` when the subscription menu item should be shown in navigation. + * This is hidden for organizations that provide premium, except if the user has premium personally + * or has a billing history. + */ + abstract canViewSubscription$(userId: UserId): Observable; + + /** + * Sets the user's premium status fields upon every full sync, either from their personal * subscription to premium, or an organization they're a part of that grants them premium. - * @param hasPremiumPersonally - * @param hasPremiumFromAnyOrganization */ abstract setHasPremium( hasPremiumPersonally: boolean, diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 8b82795fb50..928f65a3636 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,19 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response"; -import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response"; +import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; +import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; +import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; +import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; +import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; +import { InvoicesResponse } from "../models/response/invoices.response"; +import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { @@ -74,4 +75,9 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: VerifyBankAccountRequest, ) => Promise; + + restartSubscription: ( + organizationId: string, + request: OrganizationCreateRequest, + ) => Promise; } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index ddcd61573a6..1e68488ac98 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,11 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; +import { BillingSourceResponse } from "../models/response/billing.response"; +import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { name: string; @@ -57,4 +57,9 @@ export abstract class OrganizationBillingServiceAbstraction { ) => Promise; startFree: (subscription: SubscriptionInformation) => Promise; + + restartSubscription: ( + organizationId: string, + subscription: SubscriptionInformation, + ) => Promise; } diff --git a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts index 6a72724a5ec..2ed25491049 100644 --- a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts @@ -3,7 +3,7 @@ import { BillingInvoiceResponse, BillingTransactionResponse, -} from "@bitwarden/common/billing/models/response/billing.response"; +} from "../../models/response/billing.response"; export class OrganizationBillingApiServiceAbstraction { getBillingInvoices: ( diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts new file mode 100644 index 00000000000..7a744dae856 --- /dev/null +++ b/libs/common/src/billing/abstractions/tax.service.abstraction.ts @@ -0,0 +1,18 @@ +import { CountryListItem } from "../models/domain"; +import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; + +export abstract class TaxServiceAbstraction { + abstract getCountries(): CountryListItem[]; + + abstract isCountrySupported(country: string): Promise; + + abstract previewIndividualInvoice( + request: PreviewIndividualInvoiceRequest, + ): Promise; + + abstract previewOrganizationInvoice( + request: PreviewOrganizationInvoiceRequest, + ): Promise; +} diff --git a/libs/common/src/billing/models/domain/country-list-item.ts b/libs/common/src/billing/models/domain/country-list-item.ts new file mode 100644 index 00000000000..79abb96871c --- /dev/null +++ b/libs/common/src/billing/models/domain/country-list-item.ts @@ -0,0 +1,5 @@ +export type CountryListItem = { + name: string; + value: string; + disabled: boolean; +}; diff --git a/libs/common/src/billing/models/domain/index.ts b/libs/common/src/billing/models/domain/index.ts index 0f53c3e116c..057f6dc4e84 100644 --- a/libs/common/src/billing/models/domain/index.ts +++ b/libs/common/src/billing/models/domain/index.ts @@ -1,2 +1,3 @@ export * from "./bank-account"; +export * from "./country-list-item"; export * from "./tax-information"; diff --git a/libs/common/src/billing/models/domain/tax-information.ts b/libs/common/src/billing/models/domain/tax-information.ts index 78e1bcc42b5..794cdef3ed4 100644 --- a/libs/common/src/billing/models/domain/tax-information.ts +++ b/libs/common/src/billing/models/domain/tax-information.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; +import { TaxInfoResponse } from "../response/tax-info.response"; export class TaxInformation { country: string; diff --git a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts index 838249e2d8c..83b254ac512 100644 --- a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts +++ b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information"; +import { TaxInformation } from "../domain/tax-information"; import { TaxInfoUpdateRequest } from "./tax-info-update.request"; @@ -12,6 +12,10 @@ export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest { state: string; static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest { + if (!taxInformation) { + return null; + } + const request = new ExpandedTaxInfoUpdateRequest(); request.country = taxInformation.country; request.postalCode = taxInformation.postalCode; diff --git a/libs/common/src/billing/models/request/preview-individual-invoice.request.ts b/libs/common/src/billing/models/request/preview-individual-invoice.request.ts new file mode 100644 index 00000000000..f817398c629 --- /dev/null +++ b/libs/common/src/billing/models/request/preview-individual-invoice.request.ts @@ -0,0 +1,28 @@ +// @ts-strict-ignore +export class PreviewIndividualInvoiceRequest { + passwordManager: PasswordManager; + taxInformation: TaxInformation; + + constructor(passwordManager: PasswordManager, taxInformation: TaxInformation) { + this.passwordManager = passwordManager; + this.taxInformation = taxInformation; + } +} + +class PasswordManager { + additionalStorage: number; + + constructor(additionalStorage: number) { + this.additionalStorage = additionalStorage; + } +} + +class TaxInformation { + postalCode: string; + country: string; + + constructor(postalCode: string, country: string) { + this.postalCode = postalCode; + this.country = country; + } +} diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts new file mode 100644 index 00000000000..40d8db03d3b --- /dev/null +++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts @@ -0,0 +1,54 @@ +import { PlanType } from "../../enums"; + +export class PreviewOrganizationInvoiceRequest { + organizationId?: string; + passwordManager: PasswordManager; + secretsManager?: SecretsManager; + taxInformation: TaxInformation; + + constructor( + passwordManager: PasswordManager, + taxInformation: TaxInformation, + organizationId?: string, + secretsManager?: SecretsManager, + ) { + this.organizationId = organizationId; + this.passwordManager = passwordManager; + this.secretsManager = secretsManager; + this.taxInformation = taxInformation; + } +} + +class PasswordManager { + plan: PlanType; + seats: number; + additionalStorage: number; + + constructor(plan: PlanType, seats: number, additionalStorage: number) { + this.plan = plan; + this.seats = seats; + this.additionalStorage = additionalStorage; + } +} + +class SecretsManager { + seats: number; + additionalMachineAccounts: number; + + constructor(seats: number, additionalMachineAccounts: number) { + this.seats = seats; + this.additionalMachineAccounts = additionalMachineAccounts; + } +} + +class TaxInformation { + postalCode: string; + country: string; + taxId: string; + + constructor(postalCode: string, country: string, taxId: string) { + this.postalCode = postalCode; + this.country = country; + this.taxId = taxId; + } +} diff --git a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts index c740e4157ed..e4bf575cc6a 100644 --- a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts +++ b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { PaymentMethodType } from "../../enums"; export class TokenizedPaymentSourceRequest { type: PaymentMethodType; diff --git a/libs/common/src/billing/models/request/update-payment-method.request.ts b/libs/common/src/billing/models/request/update-payment-method.request.ts index 9ef91ae579b..10b03103716 100644 --- a/libs/common/src/billing/models/request/update-payment-method.request.ts +++ b/libs/common/src/billing/models/request/update-payment-method.request.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; +import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; +import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request"; export class UpdatePaymentMethodRequest { paymentSource: TokenizedPaymentSourceRequest; diff --git a/libs/common/src/billing/models/response/invoices.response.ts b/libs/common/src/billing/models/response/invoices.response.ts index bf797ba42d6..05c95b83c6f 100644 --- a/libs/common/src/billing/models/response/invoices.response.ts +++ b/libs/common/src/billing/models/response/invoices.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class InvoicesResponse extends BaseResponse { invoices: InvoiceResponse[] = []; diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts index d9733aa80f2..d30ad76a147 100644 --- a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -6,6 +6,11 @@ export class OrganizationBillingMetadataResponse extends BaseResponse { isOnSecretsManagerStandalone: boolean; isSubscriptionUnpaid: boolean; hasSubscription: boolean; + hasOpenInvoice: boolean; + invoiceDueDate: Date | null; + invoiceCreatedDate: Date | null; + subPeriodEndDate: Date | null; + isSubscriptionCanceled: boolean; constructor(response: any) { super(response); @@ -14,5 +19,15 @@ export class OrganizationBillingMetadataResponse extends BaseResponse { this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); this.isSubscriptionUnpaid = this.getResponseProperty("IsSubscriptionUnpaid"); this.hasSubscription = this.getResponseProperty("HasSubscription"); + this.hasOpenInvoice = this.getResponseProperty("HasOpenInvoice"); + + this.invoiceDueDate = this.parseDate(this.getResponseProperty("InvoiceDueDate")); + this.invoiceCreatedDate = this.parseDate(this.getResponseProperty("InvoiceCreatedDate")); + this.subPeriodEndDate = this.parseDate(this.getResponseProperty("SubPeriodEndDate")); + this.isSubscriptionCanceled = this.getResponseProperty("IsSubscriptionCanceled"); + } + + private parseDate(dateString: any): Date | null { + return dateString ? new Date(dateString) : null; } } diff --git a/libs/common/src/billing/models/response/payment-source.response.ts b/libs/common/src/billing/models/response/payment-source.response.ts index 1aeeb450b11..93418fc2f55 100644 --- a/libs/common/src/billing/models/response/payment-source.response.ts +++ b/libs/common/src/billing/models/response/payment-source.response.ts @@ -1,5 +1,5 @@ -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; +import { PaymentMethodType } from "../../enums"; export class PaymentSourceResponse extends BaseResponse { type: PaymentMethodType; diff --git a/libs/common/src/billing/models/response/preview-invoice.response.ts b/libs/common/src/billing/models/response/preview-invoice.response.ts new file mode 100644 index 00000000000..efd3da3e9f1 --- /dev/null +++ b/libs/common/src/billing/models/response/preview-invoice.response.ts @@ -0,0 +1,16 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class PreviewInvoiceResponse extends BaseResponse { + effectiveTaxRate: number; + taxableBaseAmount: number; + taxAmount: number; + totalAmount: number; + + constructor(response: any) { + super(response); + this.effectiveTaxRate = this.getResponseProperty("EffectiveTaxRate"); + this.taxableBaseAmount = this.getResponseProperty("TaxableBaseAmount"); + this.taxAmount = this.getResponseProperty("TaxAmount"); + this.totalAmount = this.getResponseProperty("TotalAmount"); + } +} diff --git a/libs/common/src/billing/models/response/provider-subscription-response.ts b/libs/common/src/billing/models/response/provider-subscription-response.ts index 2ecf988addd..4481f7588ff 100644 --- a/libs/common/src/billing/models/response/provider-subscription-response.ts +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -1,9 +1,9 @@ -import { ProviderType } from "@bitwarden/common/admin-console/enums"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { SubscriptionSuspensionResponse } from "@bitwarden/common/billing/models/response/subscription-suspension.response"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; - +import { ProviderType } from "../../../admin-console/enums"; import { BaseResponse } from "../../../models/response/base.response"; +import { PlanType, ProductTierType } from "../../enums"; + +import { SubscriptionSuspensionResponse } from "./subscription-suspension.response"; +import { TaxInfoResponse } from "./tax-info.response"; export class ProviderSubscriptionResponse extends BaseResponse { status: string; diff --git a/libs/common/src/billing/models/response/subscription-suspension.response.ts b/libs/common/src/billing/models/response/subscription-suspension.response.ts index 418e1c443c8..3d714a05dba 100644 --- a/libs/common/src/billing/models/response/subscription-suspension.response.ts +++ b/libs/common/src/billing/models/response/subscription-suspension.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class SubscriptionSuspensionResponse extends BaseResponse { suspensionDate: string; diff --git a/libs/common/src/billing/models/response/tax-id-types.response.ts b/libs/common/src/billing/models/response/tax-id-types.response.ts new file mode 100644 index 00000000000..f31f2133b34 --- /dev/null +++ b/libs/common/src/billing/models/response/tax-id-types.response.ts @@ -0,0 +1,28 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class TaxIdTypesResponse extends BaseResponse { + taxIdTypes: TaxIdTypeResponse[] = []; + + constructor(response: any) { + super(response); + const taxIdTypes = this.getResponseProperty("TaxIdTypes"); + if (taxIdTypes && taxIdTypes.length) { + this.taxIdTypes = taxIdTypes.map((t: any) => new TaxIdTypeResponse(t)); + } + } +} + +export class TaxIdTypeResponse extends BaseResponse { + code: string; + country: string; + description: string; + example: string; + + constructor(response: any) { + super(response); + this.code = this.getResponseProperty("Code"); + this.country = this.getResponseProperty("Country"); + this.description = this.getResponseProperty("Description"); + this.example = this.getResponseProperty("Example"); + } +} diff --git a/libs/common/src/billing/models/response/tax-rate.response.ts b/libs/common/src/billing/models/response/tax-rate.response.ts deleted file mode 100644 index 2c07129ba2c..00000000000 --- a/libs/common/src/billing/models/response/tax-rate.response.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BaseResponse } from "../../../models/response/base.response"; - -export class TaxRateResponse extends BaseResponse { - id: string; - country: string; - state: string; - postalCode: string; - rate: number; - - constructor(response: any) { - super(response); - this.id = this.getResponseProperty("Id"); - this.country = this.getResponseProperty("Country"); - this.state = this.getResponseProperty("State"); - this.postalCode = this.getResponseProperty("PostalCode"); - this.rate = this.getResponseProperty("Rate"); - } -} diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts index 7e0dee0eedf..02dbef469d6 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts @@ -6,8 +6,11 @@ import { FakeStateProvider, FakeSingleUserState, } from "../../../../spec"; +import { ApiService } from "../../../abstractions/api.service"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service"; +import { BillingHistoryResponse } from "../../models/response/billing-history.response"; import { BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, @@ -19,14 +22,26 @@ describe("BillingAccountProfileStateService", () => { let sut: DefaultBillingAccountProfileStateService; let userBillingAccountProfileState: FakeSingleUserState; let accountService: FakeAccountService; + let platformUtilsService: jest.Mocked; + let apiService: jest.Mocked; const userId = "fakeUserId" as UserId; beforeEach(() => { accountService = mockAccountServiceWith(userId); stateProvider = new FakeStateProvider(accountService); + platformUtilsService = { + isSelfHost: jest.fn(), + } as any; + apiService = { + getUserBillingHistory: jest.fn(), + } as any; - sut = new DefaultBillingAccountProfileStateService(stateProvider); + sut = new DefaultBillingAccountProfileStateService( + stateProvider, + platformUtilsService, + apiService, + ); userBillingAccountProfileState = stateProvider.singleUser.getFake( userId, @@ -45,7 +60,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: true, }); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(true); }); it("return false when they do not have premium from an organization", async () => { @@ -54,13 +69,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); - }); - - it("returns false when there is no active user", async () => { - await accountService.switchAccount(null); - - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false); }); }); @@ -71,7 +80,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true); }); it("returns false when the user does not have premium personally", async () => { @@ -80,13 +89,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); - }); - - it("returns false when there is no active user", async () => { - await accountService.switchAccount(null); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); + expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(false); }); }); @@ -97,7 +100,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); }); it("returns true when the user has premium from an organization", async () => { @@ -106,7 +109,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: true, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); }); it("returns true when they have premium personally AND from an organization", async () => { @@ -115,23 +118,87 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: true, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("returns false when there is no active user", async () => { - await accountService.switchAccount(null); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); }); }); describe("setHasPremium", () => { - it("should update the active users state when called", async () => { + it("should update the user's state when called", async () => { await sut.setHasPremium(true, false, userId); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false); + expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); + }); + }); + + describe("canViewSubscription$", () => { + beforeEach(() => { + platformUtilsService.isSelfHost.mockReturnValue(false); + apiService.getUserBillingHistory.mockResolvedValue( + new BillingHistoryResponse({ invoices: [], transactions: [] }), + ); + }); + + it("returns true when user has premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: true, + }); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true); + }); + + it("returns true when user has no premium from any source", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true); + }); + + it("returns true when user has billing history in cloud environment", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); + platformUtilsService.isSelfHost.mockReturnValue(false); + apiService.getUserBillingHistory.mockResolvedValue( + new BillingHistoryResponse({ + invoices: [{ id: "1" }], + transactions: [{ id: "2" }], + }), + ); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true); + }); + + it("returns false when user has no premium personally, has org premium, and no billing history", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); + platformUtilsService.isSelfHost.mockReturnValue(false); + apiService.getUserBillingHistory.mockResolvedValue( + new BillingHistoryResponse({ + invoices: [], + transactions: [], + }), + ); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false); + }); + + it("returns false when user has no premium personally, has org premium, in self-hosted environment", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); + platformUtilsService.isSelfHost.mockReturnValue(true); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false); + expect(apiService.getUserBillingHistory).not.toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts index 7d256da9714..155ce1493b4 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts @@ -1,11 +1,8 @@ -import { map, Observable, of, switchMap } from "rxjs"; +import { map, Observable, combineLatest, concatMap } from "rxjs"; -import { - ActiveUserState, - BILLING_DISK, - StateProvider, - UserKeyDefinition, -} from "../../../platform/state"; +import { ApiService } from "../../../abstractions/api.service"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; +import { BILLING_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile, @@ -22,42 +19,34 @@ export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new UserKeyDefinition; + constructor( + private readonly stateProvider: StateProvider, + private readonly platformUtilsService: PlatformUtilsService, + private readonly apiService: ApiService, + ) {} - hasPremiumFromAnyOrganization$: Observable; - hasPremiumPersonally$: Observable; - hasPremiumFromAnySource$: Observable; + hasPremiumFromAnyOrganization$(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION) + .state$.pipe(map((profile) => !!profile?.hasPremiumFromAnyOrganization)); + } - constructor(private readonly stateProvider: StateProvider) { - this.billingAccountProfileState = stateProvider.getActive( - BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, - ); + hasPremiumPersonally$(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION) + .state$.pipe(map((profile) => !!profile?.hasPremiumPersonally)); + } - // Setup an observable that will always track the currently active user - // but will fallback to emitting null when there is no active user. - const billingAccountProfileOrNull = stateProvider.activeUserId$.pipe( - switchMap((userId) => - userId != null - ? stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).state$ - : of(null), - ), - ); - - this.hasPremiumFromAnyOrganization$ = billingAccountProfileOrNull.pipe( - map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization), - ); - - this.hasPremiumPersonally$ = billingAccountProfileOrNull.pipe( - map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally), - ); - - this.hasPremiumFromAnySource$ = billingAccountProfileOrNull.pipe( - map( - (billingAccountProfile) => - billingAccountProfile?.hasPremiumFromAnyOrganization === true || - billingAccountProfile?.hasPremiumPersonally === true, - ), - ); + hasPremiumFromAnySource$(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION) + .state$.pipe( + map( + (profile) => + profile?.hasPremiumFromAnyOrganization === true || + profile?.hasPremiumPersonally === true, + ), + ); } async setHasPremium( @@ -72,4 +61,23 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP }; }); } + + canViewSubscription$(userId: UserId): Observable { + return combineLatest([ + this.hasPremiumPersonally$(userId), + this.hasPremiumFromAnyOrganization$(userId), + ]).pipe( + concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => { + const isCloud = !this.platformUtilsService.isSelfHost(); + + let billing = null; + if (isCloud) { + billing = await this.apiService.getUserBillingHistory(); + } + + const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory; + return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory; + }), + ); + } } diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index cb69f294409..4306324395e 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,24 +1,25 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response"; -import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response"; -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ToastService } from "@bitwarden/components"; import { ApiService } from "../../abstractions/api.service"; -import { BillingApiServiceAbstraction } from "../../billing/abstractions"; -import { PaymentMethodType } from "../../billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "../../billing/models/request/expanded-tax-info-update.request"; -import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; -import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; -import { PlanResponse } from "../../billing/models/response/plan.response"; +import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; +import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; +import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; +import { LogService } from "../../platform/abstractions/log.service"; +import { BillingApiServiceAbstraction } from "../abstractions"; +import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; +import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; +import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; +import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; +import { InvoicesResponse } from "../models/response/invoices.response"; +import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response"; +import { PaymentMethodResponse } from "../models/response/payment-method.response"; +import { PlanResponse } from "../models/response/plan.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { @@ -214,6 +215,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } + async restartSubscription( + organizationId: string, + request: OrganizationCreateRequest, + ): Promise { + return await this.apiService.send( + "POST", + "/organizations/" + organizationId + "/billing/restart-subscription", + request, + true, + false, + ); + } + private async execute(request: () => Promise): Promise { try { return await request(); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index a0b3782f1ad..da1a1666ff0 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,18 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - BillingApiServiceAbstraction, - OrganizationBillingServiceAbstraction, - OrganizationInformation, - PaymentInformation, - PlanInformation, - SubscriptionInformation, -} from "@bitwarden/common/billing/abstractions"; -import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -20,12 +7,25 @@ import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../ import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { SyncService } from "../../platform/sync"; import { OrgKey } from "../../types/key"; +import { + BillingApiServiceAbstraction, + OrganizationBillingServiceAbstraction, + OrganizationInformation, + PaymentInformation, + PlanInformation, + SubscriptionInformation, +} from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; +import { BillingSourceResponse } from "../models/response/billing.response"; +import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { encryptedKey: EncString; @@ -223,4 +223,17 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs request.additionalStorageGb = information.storage; } } + + async restartSubscription( + organizationId: string, + subscription: SubscriptionInformation, + ): Promise { + const request = new OrganizationCreateRequest(); + const organizationKeys = await this.makeOrganizationKeys(); + this.setOrganizationKeys(request, organizationKeys); + this.setOrganizationInformation(request, subscription.organization); + this.setPlanInformation(request, subscription.plan); + this.setPaymentInformation(request, subscription.payment); + await this.billingApiService.restartSubscription(organizationId, request); + } } diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts new file mode 100644 index 00000000000..aa27c99adc8 --- /dev/null +++ b/libs/common/src/billing/services/tax.service.ts @@ -0,0 +1,303 @@ +import { ApiService } from "../../abstractions/api.service"; +import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction"; +import { CountryListItem } from "../models/domain"; +import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; + +export class TaxService implements TaxServiceAbstraction { + constructor(private apiService: ApiService) {} + + getCountries(): CountryListItem[] { + return [ + { name: "-- Select --", value: "", disabled: false }, + { name: "United States", value: "US", disabled: false }, + { name: "China", value: "CN", disabled: false }, + { name: "France", value: "FR", disabled: false }, + { name: "Germany", value: "DE", disabled: false }, + { name: "Canada", value: "CA", disabled: false }, + { name: "United Kingdom", value: "GB", disabled: false }, + { name: "Australia", value: "AU", disabled: false }, + { name: "India", value: "IN", disabled: false }, + { name: "", value: "-", disabled: true }, + { name: "Afghanistan", value: "AF", disabled: false }, + { name: "Åland Islands", value: "AX", disabled: false }, + { name: "Albania", value: "AL", disabled: false }, + { name: "Algeria", value: "DZ", disabled: false }, + { name: "American Samoa", value: "AS", disabled: false }, + { name: "Andorra", value: "AD", disabled: false }, + { name: "Angola", value: "AO", disabled: false }, + { name: "Anguilla", value: "AI", disabled: false }, + { name: "Antarctica", value: "AQ", disabled: false }, + { name: "Antigua and Barbuda", value: "AG", disabled: false }, + { name: "Argentina", value: "AR", disabled: false }, + { name: "Armenia", value: "AM", disabled: false }, + { name: "Aruba", value: "AW", disabled: false }, + { name: "Austria", value: "AT", disabled: false }, + { name: "Azerbaijan", value: "AZ", disabled: false }, + { name: "Bahamas", value: "BS", disabled: false }, + { name: "Bahrain", value: "BH", disabled: false }, + { name: "Bangladesh", value: "BD", disabled: false }, + { name: "Barbados", value: "BB", disabled: false }, + { name: "Belarus", value: "BY", disabled: false }, + { name: "Belgium", value: "BE", disabled: false }, + { name: "Belize", value: "BZ", disabled: false }, + { name: "Benin", value: "BJ", disabled: false }, + { name: "Bermuda", value: "BM", disabled: false }, + { name: "Bhutan", value: "BT", disabled: false }, + { name: "Bolivia, Plurinational State of", value: "BO", disabled: false }, + { name: "Bonaire, Sint Eustatius and Saba", value: "BQ", disabled: false }, + { name: "Bosnia and Herzegovina", value: "BA", disabled: false }, + { name: "Botswana", value: "BW", disabled: false }, + { name: "Bouvet Island", value: "BV", disabled: false }, + { name: "Brazil", value: "BR", disabled: false }, + { name: "British Indian Ocean Territory", value: "IO", disabled: false }, + { name: "Brunei Darussalam", value: "BN", disabled: false }, + { name: "Bulgaria", value: "BG", disabled: false }, + { name: "Burkina Faso", value: "BF", disabled: false }, + { name: "Burundi", value: "BI", disabled: false }, + { name: "Cambodia", value: "KH", disabled: false }, + { name: "Cameroon", value: "CM", disabled: false }, + { name: "Cape Verde", value: "CV", disabled: false }, + { name: "Cayman Islands", value: "KY", disabled: false }, + { name: "Central African Republic", value: "CF", disabled: false }, + { name: "Chad", value: "TD", disabled: false }, + { name: "Chile", value: "CL", disabled: false }, + { name: "Christmas Island", value: "CX", disabled: false }, + { name: "Cocos (Keeling) Islands", value: "CC", disabled: false }, + { name: "Colombia", value: "CO", disabled: false }, + { name: "Comoros", value: "KM", disabled: false }, + { name: "Congo", value: "CG", disabled: false }, + { name: "Congo, the Democratic Republic of the", value: "CD", disabled: false }, + { name: "Cook Islands", value: "CK", disabled: false }, + { name: "Costa Rica", value: "CR", disabled: false }, + { name: "Côte d'Ivoire", value: "CI", disabled: false }, + { name: "Croatia", value: "HR", disabled: false }, + { name: "Cuba", value: "CU", disabled: false }, + { name: "Curaçao", value: "CW", disabled: false }, + { name: "Cyprus", value: "CY", disabled: false }, + { name: "Czech Republic", value: "CZ", disabled: false }, + { name: "Denmark", value: "DK", disabled: false }, + { name: "Djibouti", value: "DJ", disabled: false }, + { name: "Dominica", value: "DM", disabled: false }, + { name: "Dominican Republic", value: "DO", disabled: false }, + { name: "Ecuador", value: "EC", disabled: false }, + { name: "Egypt", value: "EG", disabled: false }, + { name: "El Salvador", value: "SV", disabled: false }, + { name: "Equatorial Guinea", value: "GQ", disabled: false }, + { name: "Eritrea", value: "ER", disabled: false }, + { name: "Estonia", value: "EE", disabled: false }, + { name: "Ethiopia", value: "ET", disabled: false }, + { name: "Falkland Islands (Malvinas)", value: "FK", disabled: false }, + { name: "Faroe Islands", value: "FO", disabled: false }, + { name: "Fiji", value: "FJ", disabled: false }, + { name: "Finland", value: "FI", disabled: false }, + { name: "French Guiana", value: "GF", disabled: false }, + { name: "French Polynesia", value: "PF", disabled: false }, + { name: "French Southern Territories", value: "TF", disabled: false }, + { name: "Gabon", value: "GA", disabled: false }, + { name: "Gambia", value: "GM", disabled: false }, + { name: "Georgia", value: "GE", disabled: false }, + { name: "Ghana", value: "GH", disabled: false }, + { name: "Gibraltar", value: "GI", disabled: false }, + { name: "Greece", value: "GR", disabled: false }, + { name: "Greenland", value: "GL", disabled: false }, + { name: "Grenada", value: "GD", disabled: false }, + { name: "Guadeloupe", value: "GP", disabled: false }, + { name: "Guam", value: "GU", disabled: false }, + { name: "Guatemala", value: "GT", disabled: false }, + { name: "Guernsey", value: "GG", disabled: false }, + { name: "Guinea", value: "GN", disabled: false }, + { name: "Guinea-Bissau", value: "GW", disabled: false }, + { name: "Guyana", value: "GY", disabled: false }, + { name: "Haiti", value: "HT", disabled: false }, + { name: "Heard Island and McDonald Islands", value: "HM", disabled: false }, + { name: "Holy See (Vatican City State)", value: "VA", disabled: false }, + { name: "Honduras", value: "HN", disabled: false }, + { name: "Hong Kong", value: "HK", disabled: false }, + { name: "Hungary", value: "HU", disabled: false }, + { name: "Iceland", value: "IS", disabled: false }, + { name: "Indonesia", value: "ID", disabled: false }, + { name: "Iran, Islamic Republic of", value: "IR", disabled: false }, + { name: "Iraq", value: "IQ", disabled: false }, + { name: "Ireland", value: "IE", disabled: false }, + { name: "Isle of Man", value: "IM", disabled: false }, + { name: "Israel", value: "IL", disabled: false }, + { name: "Italy", value: "IT", disabled: false }, + { name: "Jamaica", value: "JM", disabled: false }, + { name: "Japan", value: "JP", disabled: false }, + { name: "Jersey", value: "JE", disabled: false }, + { name: "Jordan", value: "JO", disabled: false }, + { name: "Kazakhstan", value: "KZ", disabled: false }, + { name: "Kenya", value: "KE", disabled: false }, + { name: "Kiribati", value: "KI", disabled: false }, + { name: "Korea, Democratic People's Republic of", value: "KP", disabled: false }, + { name: "Korea, Republic of", value: "KR", disabled: false }, + { name: "Kuwait", value: "KW", disabled: false }, + { name: "Kyrgyzstan", value: "KG", disabled: false }, + { name: "Lao People's Democratic Republic", value: "LA", disabled: false }, + { name: "Latvia", value: "LV", disabled: false }, + { name: "Lebanon", value: "LB", disabled: false }, + { name: "Lesotho", value: "LS", disabled: false }, + { name: "Liberia", value: "LR", disabled: false }, + { name: "Libya", value: "LY", disabled: false }, + { name: "Liechtenstein", value: "LI", disabled: false }, + { name: "Lithuania", value: "LT", disabled: false }, + { name: "Luxembourg", value: "LU", disabled: false }, + { name: "Macao", value: "MO", disabled: false }, + { name: "Macedonia, the former Yugoslav Republic of", value: "MK", disabled: false }, + { name: "Madagascar", value: "MG", disabled: false }, + { name: "Malawi", value: "MW", disabled: false }, + { name: "Malaysia", value: "MY", disabled: false }, + { name: "Maldives", value: "MV", disabled: false }, + { name: "Mali", value: "ML", disabled: false }, + { name: "Malta", value: "MT", disabled: false }, + { name: "Marshall Islands", value: "MH", disabled: false }, + { name: "Martinique", value: "MQ", disabled: false }, + { name: "Mauritania", value: "MR", disabled: false }, + { name: "Mauritius", value: "MU", disabled: false }, + { name: "Mayotte", value: "YT", disabled: false }, + { name: "Mexico", value: "MX", disabled: false }, + { name: "Micronesia, Federated States of", value: "FM", disabled: false }, + { name: "Moldova, Republic of", value: "MD", disabled: false }, + { name: "Monaco", value: "MC", disabled: false }, + { name: "Mongolia", value: "MN", disabled: false }, + { name: "Montenegro", value: "ME", disabled: false }, + { name: "Montserrat", value: "MS", disabled: false }, + { name: "Morocco", value: "MA", disabled: false }, + { name: "Mozambique", value: "MZ", disabled: false }, + { name: "Myanmar", value: "MM", disabled: false }, + { name: "Namibia", value: "NA", disabled: false }, + { name: "Nauru", value: "NR", disabled: false }, + { name: "Nepal", value: "NP", disabled: false }, + { name: "Netherlands", value: "NL", disabled: false }, + { name: "New Caledonia", value: "NC", disabled: false }, + { name: "New Zealand", value: "NZ", disabled: false }, + { name: "Nicaragua", value: "NI", disabled: false }, + { name: "Niger", value: "NE", disabled: false }, + { name: "Nigeria", value: "NG", disabled: false }, + { name: "Niue", value: "NU", disabled: false }, + { name: "Norfolk Island", value: "NF", disabled: false }, + { name: "Northern Mariana Islands", value: "MP", disabled: false }, + { name: "Norway", value: "NO", disabled: false }, + { name: "Oman", value: "OM", disabled: false }, + { name: "Pakistan", value: "PK", disabled: false }, + { name: "Palau", value: "PW", disabled: false }, + { name: "Palestinian Territory, Occupied", value: "PS", disabled: false }, + { name: "Panama", value: "PA", disabled: false }, + { name: "Papua New Guinea", value: "PG", disabled: false }, + { name: "Paraguay", value: "PY", disabled: false }, + { name: "Peru", value: "PE", disabled: false }, + { name: "Philippines", value: "PH", disabled: false }, + { name: "Pitcairn", value: "PN", disabled: false }, + { name: "Poland", value: "PL", disabled: false }, + { name: "Portugal", value: "PT", disabled: false }, + { name: "Puerto Rico", value: "PR", disabled: false }, + { name: "Qatar", value: "QA", disabled: false }, + { name: "Réunion", value: "RE", disabled: false }, + { name: "Romania", value: "RO", disabled: false }, + { name: "Russian Federation", value: "RU", disabled: false }, + { name: "Rwanda", value: "RW", disabled: false }, + { name: "Saint Barthélemy", value: "BL", disabled: false }, + { name: "Saint Helena, Ascension and Tristan da Cunha", value: "SH", disabled: false }, + { name: "Saint Kitts and Nevis", value: "KN", disabled: false }, + { name: "Saint Lucia", value: "LC", disabled: false }, + { name: "Saint Martin (French part)", value: "MF", disabled: false }, + { name: "Saint Pierre and Miquelon", value: "PM", disabled: false }, + { name: "Saint Vincent and the Grenadines", value: "VC", disabled: false }, + { name: "Samoa", value: "WS", disabled: false }, + { name: "San Marino", value: "SM", disabled: false }, + { name: "Sao Tome and Principe", value: "ST", disabled: false }, + { name: "Saudi Arabia", value: "SA", disabled: false }, + { name: "Senegal", value: "SN", disabled: false }, + { name: "Serbia", value: "RS", disabled: false }, + { name: "Seychelles", value: "SC", disabled: false }, + { name: "Sierra Leone", value: "SL", disabled: false }, + { name: "Singapore", value: "SG", disabled: false }, + { name: "Sint Maarten (Dutch part)", value: "SX", disabled: false }, + { name: "Slovakia", value: "SK", disabled: false }, + { name: "Slovenia", value: "SI", disabled: false }, + { name: "Solomon Islands", value: "SB", disabled: false }, + { name: "Somalia", value: "SO", disabled: false }, + { name: "South Africa", value: "ZA", disabled: false }, + { name: "South Georgia and the South Sandwich Islands", value: "GS", disabled: false }, + { name: "South Sudan", value: "SS", disabled: false }, + { name: "Spain", value: "ES", disabled: false }, + { name: "Sri Lanka", value: "LK", disabled: false }, + { name: "Sudan", value: "SD", disabled: false }, + { name: "Suriname", value: "SR", disabled: false }, + { name: "Svalbard and Jan Mayen", value: "SJ", disabled: false }, + { name: "Swaziland", value: "SZ", disabled: false }, + { name: "Sweden", value: "SE", disabled: false }, + { name: "Switzerland", value: "CH", disabled: false }, + { name: "Syrian Arab Republic", value: "SY", disabled: false }, + { name: "Taiwan", value: "TW", disabled: false }, + { name: "Tajikistan", value: "TJ", disabled: false }, + { name: "Tanzania, United Republic of", value: "TZ", disabled: false }, + { name: "Thailand", value: "TH", disabled: false }, + { name: "Timor-Leste", value: "TL", disabled: false }, + { name: "Togo", value: "TG", disabled: false }, + { name: "Tokelau", value: "TK", disabled: false }, + { name: "Tonga", value: "TO", disabled: false }, + { name: "Trinidad and Tobago", value: "TT", disabled: false }, + { name: "Tunisia", value: "TN", disabled: false }, + { name: "Turkey", value: "TR", disabled: false }, + { name: "Turkmenistan", value: "TM", disabled: false }, + { name: "Turks and Caicos Islands", value: "TC", disabled: false }, + { name: "Tuvalu", value: "TV", disabled: false }, + { name: "Uganda", value: "UG", disabled: false }, + { name: "Ukraine", value: "UA", disabled: false }, + { name: "United Arab Emirates", value: "AE", disabled: false }, + { name: "United States Minor Outlying Islands", value: "UM", disabled: false }, + { name: "Uruguay", value: "UY", disabled: false }, + { name: "Uzbekistan", value: "UZ", disabled: false }, + { name: "Vanuatu", value: "VU", disabled: false }, + { name: "Venezuela, Bolivarian Republic of", value: "VE", disabled: false }, + { name: "Viet Nam", value: "VN", disabled: false }, + { name: "Virgin Islands, British", value: "VG", disabled: false }, + { name: "Virgin Islands, U.S.", value: "VI", disabled: false }, + { name: "Wallis and Futuna", value: "WF", disabled: false }, + { name: "Western Sahara", value: "EH", disabled: false }, + { name: "Yemen", value: "YE", disabled: false }, + { name: "Zambia", value: "ZM", disabled: false }, + { name: "Zimbabwe", value: "ZW", disabled: false }, + ]; + } + + async isCountrySupported(country: string): Promise { + const response = await this.apiService.send( + "GET", + "/tax/is-country-supported?country=" + country, + null, + true, + true, + ); + return response; + } + + async previewIndividualInvoice( + request: PreviewIndividualInvoiceRequest, + ): Promise { + const response = await this.apiService.send( + "POST", + "/accounts/billing/preview-invoice", + request, + true, + true, + ); + return new PreviewInvoiceResponse(response); + } + + async previewOrganizationInvoice( + request: PreviewOrganizationInvoiceRequest, + ): Promise { + const response = await this.apiService.send( + "POST", + `/invoices/preview-organization`, + request, + true, + true, + ); + return new PreviewInvoiceResponse(response); + } +} diff --git a/libs/common/src/enums/device-type.enum.ts b/libs/common/src/enums/device-type.enum.ts index 1b8574a4c49..ff6329b9ac4 100644 --- a/libs/common/src/enums/device-type.enum.ts +++ b/libs/common/src/enums/device-type.enum.ts @@ -27,18 +27,40 @@ export enum DeviceType { LinuxCLI = 25, } -export const MobileDeviceTypes: Set = new Set([ - DeviceType.Android, - DeviceType.iOS, - DeviceType.AndroidAmazon, -]); +/** + * Device type metadata + * Each device type has a category corresponding to the client type and platform (Android, iOS, Chrome, Firefox, etc.) + */ +interface DeviceTypeMetadata { + category: "mobile" | "extension" | "webVault" | "desktop" | "cli" | "sdk" | "server"; + platform: string; +} -export const DesktopDeviceTypes: Set = new Set([ - DeviceType.WindowsDesktop, - DeviceType.MacOsDesktop, - DeviceType.LinuxDesktop, - DeviceType.UWP, - DeviceType.WindowsCLI, - DeviceType.MacOsCLI, - DeviceType.LinuxCLI, -]); +export const DeviceTypeMetadata: Record = { + [DeviceType.Android]: { category: "mobile", platform: "Android" }, + [DeviceType.iOS]: { category: "mobile", platform: "iOS" }, + [DeviceType.AndroidAmazon]: { category: "mobile", platform: "Amazon" }, + [DeviceType.ChromeExtension]: { category: "extension", platform: "Chrome" }, + [DeviceType.FirefoxExtension]: { category: "extension", platform: "Firefox" }, + [DeviceType.OperaExtension]: { category: "extension", platform: "Opera" }, + [DeviceType.EdgeExtension]: { category: "extension", platform: "Edge" }, + [DeviceType.VivaldiExtension]: { category: "extension", platform: "Vivaldi" }, + [DeviceType.SafariExtension]: { category: "extension", platform: "Safari" }, + [DeviceType.ChromeBrowser]: { category: "webVault", platform: "Chrome" }, + [DeviceType.FirefoxBrowser]: { category: "webVault", platform: "Firefox" }, + [DeviceType.OperaBrowser]: { category: "webVault", platform: "Opera" }, + [DeviceType.EdgeBrowser]: { category: "webVault", platform: "Edge" }, + [DeviceType.IEBrowser]: { category: "webVault", platform: "IE" }, + [DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" }, + [DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" }, + [DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" }, + [DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" }, + [DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" }, + [DeviceType.LinuxDesktop]: { category: "desktop", platform: "Linux" }, + [DeviceType.UWP]: { category: "desktop", platform: "Windows UWP" }, + [DeviceType.WindowsCLI]: { category: "cli", platform: "Windows" }, + [DeviceType.MacOsCLI]: { category: "cli", platform: "macOS" }, + [DeviceType.LinuxCLI]: { category: "cli", platform: "Linux" }, + [DeviceType.SDK]: { category: "sdk", platform: "" }, + [DeviceType.Server]: { category: "server", platform: "" }, +}; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index cc2abed3ba1..95d174dd2ad 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -4,29 +4,32 @@ * Flags MUST be short lived and SHALL be removed once enabled. */ export enum FeatureFlag { - BrowserFilelessImport = "browser-fileless-import", + /* Autofill */ + BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", + DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", + EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", + GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", + IdpAutoSubmitLogin = "idp-auto-submit-login", + InlineMenuFieldQualification = "inline-menu-field-qualification", + InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", + InlineMenuTotp = "inline-menu-totp", + NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements", + NotificationRefresh = "notification-refresh", + UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", + ItemShare = "item-share", GeneratorToolsModernization = "generator-tools-modernization", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", - UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", - EmailVerification = "email-verification", - InlineMenuFieldQualification = "inline-menu-field-qualification", TwoFactorComponentRefactor = "two-factor-component-refactor", - InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", - IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", - GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", - EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", - DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", AccountDeprovisioning = "pm-10308-account-deprovisioning", SSHKeyVaultItem = "ssh-key-vault-item", SSHAgent = "ssh-agent", - NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements", AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", @@ -38,11 +41,9 @@ export enum FeatureFlag { NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", - InlineMenuTotp = "inline-menu-totp", MacOsNativeCredentialSync = "macos-native-credential-sync", - PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", - PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", PrivateKeyRegeneration = "pm-12241-private-key-regeneration", + ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -57,29 +58,32 @@ const FALSE = false as boolean; * We support true as a value as we prefer flags to "enable" not "disable". */ export const DefaultFeatureFlagValue = { - [FeatureFlag.BrowserFilelessImport]: FALSE, + /* Autofill */ + [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, + [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, + [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, + [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, + [FeatureFlag.IdpAutoSubmitLogin]: FALSE, + [FeatureFlag.InlineMenuFieldQualification]: FALSE, + [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, + [FeatureFlag.InlineMenuTotp]: FALSE, + [FeatureFlag.NotificationBarAddLoginImprovements]: FALSE, + [FeatureFlag.NotificationRefresh]: FALSE, + [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, + [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.GeneratorToolsModernization]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, - [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, - [FeatureFlag.EmailVerification]: FALSE, - [FeatureFlag.InlineMenuFieldQualification]: FALSE, [FeatureFlag.TwoFactorComponentRefactor]: FALSE, - [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, - [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, - [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, - [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, - [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, [FeatureFlag.AccountDeprovisioning]: FALSE, [FeatureFlag.SSHKeyVaultItem]: FALSE, [FeatureFlag.SSHAgent]: FALSE, - [FeatureFlag.NotificationBarAddLoginImprovements]: FALSE, [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, @@ -91,11 +95,9 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, - [FeatureFlag.InlineMenuTotp]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, - [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, - [FeatureFlag.PM12443RemovePagingLogic]: FALSE, [FeatureFlag.PrivateKeyRegeneration]: FALSE, + [FeatureFlag.ResellerManagedOrgAlert]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index 69cbdff9dd2..db59fcafa69 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -23,4 +23,5 @@ export enum NotificationType { SyncOrganizations = 17, SyncOrganizationStatusChanged = 18, + SyncOrganizationCollectionSettingChanged = 19, } diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts index 961d199b06e..9f97e0a94c1 100644 --- a/libs/common/src/key-management/services/default-process-reload.service.ts +++ b/libs/common/src/key-management/services/default-process-reload.service.ts @@ -2,15 +2,18 @@ // @ts-strict-ignore import { firstValueFrom, map, timeout } from "rxjs"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; +import { MessagingService } from "../../platform/abstractions/messaging.service"; import { UserId } from "../../types/guid"; import { ProcessReloadServiceAbstraction } from "../abstractions/process-reload.service"; @@ -24,6 +27,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, private accountService: AccountService, + private logService: LogService, ) {} async startProcessReload(authService: AuthService): Promise { @@ -35,6 +39,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); status = await authService.getAuthStatus(userId); if (status === AuthenticationStatus.Unlocked) { + this.logService.info( + "[Process Reload Service] User unlocked, preventing process reload", + ); return; } } @@ -51,6 +58,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract if (userId != null) { const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); if (ephemeralPin != null) { + this.logService.info( + "[Process Reload Service] Ephemeral pin active, preventing process reload", + ); return; } } @@ -93,7 +103,12 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract await this.reloadCallback(); } return; + } else { + this.logService.info( + "[Process Reload Service] Desktop ipc fingerprint validated, preventing process reload", + ); } + if (this.reloadInterval == null) { this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); } diff --git a/libs/common/src/models/domain/domain-service.ts b/libs/common/src/models/domain/domain-service.ts index 9ff53cc8787..a6b5ecfdaac 100644 --- a/libs/common/src/models/domain/domain-service.ts +++ b/libs/common/src/models/domain/domain-service.ts @@ -21,5 +21,5 @@ export const UriMatchStrategy = { export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy]; // using uniqueness properties of object shape over Set for ease of state storability -export type NeverDomains = { [id: string]: null }; +export type NeverDomains = { [id: string]: null | { bannerIsDismissed?: boolean } }; export type EquivalentDomains = string[][]; diff --git a/libs/common/src/models/export/ssh-key.export.ts b/libs/common/src/models/export/ssh-key.export.ts index c502ddafd47..a99ebac34b3 100644 --- a/libs/common/src/models/export/ssh-key.export.ts +++ b/libs/common/src/models/export/ssh-key.export.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { SshKeyView as SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { EncString } from "../../platform/models/domain/enc-string"; import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key"; +import { SshKeyView as SshKeyView } from "../../vault/models/view/ssh-key.view"; import { safeGetString } from "./utils"; diff --git a/libs/common/src/models/export/utils.ts b/libs/common/src/models/export/utils.ts index 630b4898503..b7e0b74b611 100644 --- a/libs/common/src/models/export/utils.ts +++ b/libs/common/src/models/export/utils.ts @@ -1,4 +1,4 @@ -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncString } from "../../platform/models/domain/enc-string"; export function safeGetString(value: string | EncString) { if (value == null) { diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index 473e6fc1d10..894a00ee885 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -45,6 +45,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncOrganizationStatusChanged: this.payload = new OrganizationStatusPushNotification(payload); break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + this.payload = new OrganizationCollectionSettingChangedPushNotification(payload); + break; default: break; } @@ -126,3 +129,17 @@ export class OrganizationStatusPushNotification extends BaseResponse { this.enabled = this.getResponseProperty("Enabled"); } } + +export class OrganizationCollectionSettingChangedPushNotification extends BaseResponse { + organizationId: string; + limitCollectionCreation: boolean; + limitCollectionDeletion: boolean; + + constructor(response: any) { + super(response); + + this.organizationId = this.getResponseProperty("OrganizationId"); + this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation"); + this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion"); + } +} diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index 18c14677dd0..56b0ee55afe 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -1,5 +1,5 @@ import { CsprngArray } from "../../types/csprng"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoFunctionService { @@ -51,11 +51,13 @@ export abstract class CryptoFunctionService { iv: string, mac: string, key: SymmetricCryptoKey, - ): DecryptParameters; - abstract aesDecryptFast( - parameters: DecryptParameters, - mode: "cbc" | "ecb", - ): Promise; + ): CbcDecryptParameters; + abstract aesDecryptFast({ + mode, + parameters, + }: + | { mode: "cbc"; parameters: CbcDecryptParameters } + | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise; abstract aesDecrypt( data: Uint8Array, iv: Uint8Array, diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index 5b28b98803b..a660524699d 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -8,12 +8,32 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an EncString to a string + * @param encString - The EncString to decrypt + * @param key - The key to decrypt the EncString with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted string + */ abstract decryptToUtf8( encString: EncString, key: SymmetricCryptoKey, - decryptContext?: string, + decryptTrace?: string, ): Promise; - abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an Encrypted object to a Uint8Array + * @param encThing - The Encrypted object to decrypt + * @param key - The key to decrypt the Encrypted object with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted Uint8Array + */ + abstract decryptToBytes( + encThing: Encrypted, + key: SymmetricCryptoKey, + decryptTrace?: string, + ): Promise; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; diff --git a/libs/common/src/platform/enums/key-suffix-options.enum.ts b/libs/common/src/platform/enums/key-suffix-options.enum.ts index b268c4b777f..98fa215be6a 100644 --- a/libs/common/src/platform/enums/key-suffix-options.enum.ts +++ b/libs/common/src/platform/enums/key-suffix-options.enum.ts @@ -1,5 +1,4 @@ export enum KeySuffixOptions { Auto = "auto", - Biometric = "biometric", Pin = "pin", } diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index b52879d88fa..30531b6799e 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -1,18 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type SharedFlags = { sdk?: boolean; prereleaseBuild?: boolean; }; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type SharedDevFlags = { noopNotifications: boolean; skipWelcomeOnInstall: boolean; configRetrievalIntervalMs: number; + showRiskInsightsDebug: boolean; }; function getFlags(envFlags: string | T): T { diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index d2bf11a20ce..f654897e9e2 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -8,10 +8,14 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports const nodeURL = typeof self === "undefined" ? require("url") : null; declare global { @@ -608,6 +612,8 @@ export class Utils { } return new URL(uriString); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Ignore error } diff --git a/libs/common/src/platform/models/domain/decrypt-parameters.ts b/libs/common/src/platform/models/domain/decrypt-parameters.ts index 784826d3bd2..d3b4bf60d42 100644 --- a/libs/common/src/platform/models/domain/decrypt-parameters.ts +++ b/libs/common/src/platform/models/domain/decrypt-parameters.ts @@ -1,10 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -export class DecryptParameters { +export type CbcDecryptParameters = { encKey: T; data: T; iv: T; - macKey: T; - mac: T; + macKey?: T; + mac?: T; macData: T; -} +}; + +export type EcbDecryptParameters = { + encKey: T; + data: T; +}; diff --git a/libs/common/src/platform/models/domain/domain-base.spec.ts b/libs/common/src/platform/models/domain/domain-base.spec.ts index 0bdee21e3c1..80a4e5e8606 100644 --- a/libs/common/src/platform/models/domain/domain-base.spec.ts +++ b/libs/common/src/platform/models/domain/domain-base.spec.ts @@ -67,9 +67,13 @@ describe("DomainBase", () => { ); // @ts-expect-error -- encString2 was not decrypted + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions decrypted as { encToString: string; encString2: string; plainText: string }; // encString2 was not decrypted, so it's still an EncString + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions decrypted as { encToString: string; encString2: EncString; plainText: string }; }); diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index bd9139999b7..110a1dc7208 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -8,7 +8,7 @@ import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type type EncStringKeys = ConditionalKeys, EncString>; export type DecryptedObject< TEncryptedObject, @@ -63,6 +63,7 @@ export default class Domain { map: any, orgId: string, key: SymmetricCryptoKey = null, + objectContext: string = "No Domain Context", ): Promise { const promises = []; const self: any = this; @@ -78,7 +79,11 @@ export default class Domain { .then(() => { const mapProp = map[theProp] || theProp; if (self[mapProp]) { - return self[mapProp].decrypt(orgId, key); + return self[mapProp].decrypt( + orgId, + key, + `Property: ${prop}; ObjectContext: ${objectContext}`, + ); } return null; }) @@ -114,12 +119,21 @@ export default class Domain { key: SymmetricCryptoKey, encryptService: EncryptService, _: Constructor = this.constructor as Constructor, + objectContext: string = "No Domain Context", ): Promise> { const promises = []; for (const prop of encryptedProperties) { const value = (this as any)[prop] as EncString; - promises.push(this.decryptProperty(prop, value, key, encryptService)); + promises.push( + this.decryptProperty( + prop, + value, + key, + encryptService, + `Property: ${prop.toString()}; ObjectContext: ${objectContext}`, + ), + ); } const decryptedObjects = await Promise.all(promises); @@ -137,10 +151,11 @@ export default class Domain { value: EncString, key: SymmetricCryptoKey, encryptService: EncryptService, + decryptTrace: string, ) { let decrypted: string = null; if (value) { - decrypted = await value.decryptWithKey(key, encryptService); + decrypted = await value.decryptWithKey(key, encryptService, decryptTrace); } else { decrypted = null; } diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index 85108a9609b..b4916b9f70a 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeEncString, makeStaticByteArray } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index c484c80ee5b..f148664a4f9 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -125,6 +125,8 @@ export class EncString implements Encrypted { try { encType = parseInt(headerPieces[0], null); encPieces = headerPieces[1].split("|"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return { encType: NaN, encPieces: [] }; } @@ -156,21 +158,21 @@ export class EncString implements Encrypted { return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length; } - async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise { + async decrypt(orgId: string, key: SymmetricCryptoKey = null, context?: string): Promise { if (this.decryptedValue != null) { return this.decryptedValue; } - let keyContext = "provided-key"; + let decryptTrace = "provided-key"; try { if (key == null) { key = await this.getKeyForDecryption(orgId); - keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey"; + decryptTrace = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey"; if (orgId != null) { - keyContext = `domain-orgkey-${orgId}`; + decryptTrace = `domain-orgkey-${orgId}`; } else { const cryptoService = Utils.getContainerService().getKeyService(); - keyContext = + decryptTrace = (await cryptoService.getUserKey()) == null ? "domain-withlegacysupport-masterkey" : "domain-withlegacysupport-userkey"; @@ -181,20 +183,32 @@ export class EncString implements Encrypted { } const encryptService = Utils.getContainerService().getEncryptService(); - this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext); + this.decryptedValue = await encryptService.decryptToUtf8( + this, + key, + decryptTrace == null ? context : `${decryptTrace}${context || ""}`, + ); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.decryptedValue = DECRYPT_ERROR; } return this.decryptedValue; } - async decryptWithKey(key: SymmetricCryptoKey, encryptService: EncryptService) { + async decryptWithKey( + key: SymmetricCryptoKey, + encryptService: EncryptService, + decryptTrace: string = "domain-withkey", + ): Promise { try { if (key == null) { throw new Error("No key to decrypt EncString"); } - this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey"); + this.decryptedValue = await encryptService.decryptToUtf8(this, key, decryptTrace); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.decryptedValue = DECRYPT_ERROR; } diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index f467cb8d6e4..eab4c7b2114 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -7,7 +7,7 @@ import { EncryptionType } from "../../enums"; export class SymmetricCryptoKey { key: Uint8Array; - encKey?: Uint8Array; + encKey: Uint8Array; macKey?: Uint8Array; encType: EncryptionType; @@ -48,12 +48,8 @@ export class SymmetricCryptoKey { throw new Error("Unsupported encType/key length."); } - if (this.key != null) { - this.keyB64 = Utils.fromBufferToB64(this.key); - } - if (this.encKey != null) { - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - } + this.keyB64 = Utils.fromBufferToB64(this.key); + this.encKeyB64 = Utils.fromBufferToB64(this.encKey); if (this.macKey != null) { this.macKeyB64 = Utils.fromBufferToB64(this.macKey); } diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts index 6022e097ab0..c3e727a2e1e 100644 --- a/libs/common/src/platform/services/container.service.ts +++ b/libs/common/src/platform/services/container.service.ts @@ -1,3 +1,5 @@ +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index 0a85b34eba8..68263cadf27 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -114,7 +114,7 @@ export class EncryptServiceImplementation implements EncryptService { const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { this.logMacFailed( - "[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " + + "[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " + encryptionTypeName(key.encType) + "Payload type " + encryptionTypeName(encString.encryptionType) + @@ -125,10 +125,14 @@ export class EncryptServiceImplementation implements EncryptService { } } - return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc"); + return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams }); } - async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise { + async decryptToBytes( + encThing: Encrypted, + key: SymmetricCryptoKey, + decryptContext: string = "no context", + ): Promise { if (key == null) { throw new Error("No encryption key provided."); } @@ -145,7 +149,9 @@ export class EncryptServiceImplementation implements EncryptService { "[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -155,7 +161,9 @@ export class EncryptServiceImplementation implements EncryptService { "[Encrypt service] Key encryption type does not match payload encryption type. Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -167,11 +175,13 @@ export class EncryptServiceImplementation implements EncryptService { const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256"); if (computedMac === null) { this.logMacFailed( - "[Encrypt service] Failed to compute MAC." + + "[Encrypt service#decryptToBytes] Failed to compute MAC." + " Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -179,11 +189,13 @@ export class EncryptServiceImplementation implements EncryptService { const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac); if (!macsMatch) { this.logMacFailed( - "[Encrypt service] MAC comparison failed. Key or payload has changed." + + "[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." + " Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } diff --git a/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts b/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts index ce9426b4db0..8089e081805 100644 --- a/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts @@ -1,14 +1,18 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, Observable } from "rxjs"; -// FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths -import { flushPromises } from "@bitwarden/browser/src/autofill/spec/testing-utils"; - import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view"; import { Fido2ActiveRequestManager } from "./fido2-active-request-manager"; +// Duplicated from `apps/browser/src/autofill/spec/testing-utils.ts`. +const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout; +function flushPromises() { + return new Promise(function (resolve) { + scheduler(resolve); + }); +} + jest.mock("rxjs", () => { const rxjs = jest.requireActual("rxjs"); const { firstValueFrom } = rxjs; diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts index 23a8ba3138b..16b3968045a 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DefaultKeyService } from "../../../../key-management/src/key.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts index abb8993c39c..a8947a49f45 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -1,3 +1,5 @@ +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { UserId } from "../../types/guid"; import { KeySuffixOptions } from "../enums"; diff --git a/libs/common/src/platform/services/web-crypto-function.service.spec.ts b/libs/common/src/platform/services/web-crypto-function.service.spec.ts index 71f2828855f..1929e6454ef 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.spec.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.spec.ts @@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended"; import { Utils } from "../../platform/misc/utils"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { WebCryptoFunctionService } from "./web-crypto-function.service"; @@ -253,8 +253,13 @@ describe("WebCrypto Function Service", () => { const encData = Utils.fromBufferToB64(encValue); const b64Iv = Utils.fromBufferToB64(iv); const symKey = new SymmetricCryptoKey(key); - const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); + const parameters = cryptoFunctionService.aesDecryptFastParameters( + encData, + b64Iv, + null, + symKey, + ); + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters }); expect(decValue).toBe(value); }); @@ -276,8 +281,8 @@ describe("WebCrypto Function Service", () => { const iv = Utils.fromBufferToB64(makeStaticByteArray(16)); const symKey = new SymmetricCryptoKey(makeStaticByteArray(32)); const data = "ByUF8vhyX4ddU9gcooznwA=="; - const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); + const parameters = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters }); expect(decValue).toBe("EncryptMe!"); }); }); @@ -287,10 +292,11 @@ describe("WebCrypto Function Service", () => { const cryptoFunctionService = getWebCryptoFunctionService(); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="); - const params = new DecryptParameters(); - params.encKey = Utils.fromBufferToByteString(key); - params.data = Utils.fromBufferToByteString(data); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "ecb"); + const parameters: EcbDecryptParameters = { + encKey: Utils.fromBufferToByteString(key), + data: Utils.fromBufferToByteString(data), + }; + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "ecb", parameters }); expect(decValue).toBe("EncryptMe!"); }); }); @@ -304,6 +310,15 @@ describe("WebCrypto Function Service", () => { const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "cbc"); expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); }); + + it("throws if iv is not provided", async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = makeStaticByteArray(32); + const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA=="); + await expect(() => cryptoFunctionService.aesDecrypt(data, null, key, "cbc")).rejects.toThrow( + "IV is required for CBC mode", + ); + }); }); describe("aesDecrypt ECB mode", () => { diff --git a/libs/common/src/platform/services/web-crypto-function.service.ts b/libs/common/src/platform/services/web-crypto-function.service.ts index c0592654849..61edf7a13b1 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.ts @@ -1,12 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import * as argon2 from "argon2-browser"; import * as forge from "node-forge"; import { Utils } from "../../platform/misc/utils"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export class WebCryptoFunctionService implements CryptoFunctionService { @@ -14,10 +12,14 @@ export class WebCryptoFunctionService implements CryptoFunctionService { private subtle: SubtleCrypto; private wasmSupported: boolean; - constructor(globalContext: Window | typeof global) { - this.crypto = typeof globalContext.crypto !== "undefined" ? globalContext.crypto : null; - this.subtle = - !!this.crypto && typeof this.crypto.subtle !== "undefined" ? this.crypto.subtle : null; + constructor(globalContext: { crypto: Crypto }) { + if (globalContext?.crypto?.subtle == null) { + throw new Error( + "Could not instantiate WebCryptoFunctionService. Could not locate Subtle crypto.", + ); + } + this.crypto = globalContext.crypto; + this.subtle = this.crypto.subtle; this.wasmSupported = this.checkIfWasmSupported(); } @@ -220,7 +222,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { hmac.update(a); const mac1 = hmac.digest().getBytes(); - hmac.start(null, null); + hmac.start("sha256", null); hmac.update(b); const mac2 = hmac.digest().getBytes(); @@ -239,10 +241,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService { aesDecryptFastParameters( data: string, iv: string, - mac: string, + mac: string | null, key: SymmetricCryptoKey, - ): DecryptParameters { - const p = new DecryptParameters(); + ): CbcDecryptParameters { + const p = {} as CbcDecryptParameters; if (key.meta != null) { p.encKey = key.meta.encKeyByteString; p.macKey = key.meta.macKeyByteString; @@ -275,7 +277,12 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return p; } - aesDecryptFast(parameters: DecryptParameters, mode: "cbc" | "ecb"): Promise { + aesDecryptFast({ + mode, + parameters, + }: + | { mode: "cbc"; parameters: CbcDecryptParameters } + | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise { const decipher = (forge as any).cipher.createDecipher( this.toWebCryptoAesMode(mode), parameters.encKey, @@ -294,21 +301,27 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async aesDecrypt( data: Uint8Array, - iv: Uint8Array, + iv: Uint8Array | null, key: Uint8Array, mode: "cbc" | "ecb", ): Promise { if (mode === "ecb") { // Web crypto does not support AES-ECB mode, so we need to do this in forge. - const params = new DecryptParameters(); - params.data = this.toByteString(data); - params.encKey = this.toByteString(key); - const result = await this.aesDecryptFast(params, "ecb"); + const parameters: EcbDecryptParameters = { + data: this.toByteString(data), + encKey: this.toByteString(key), + }; + const result = await this.aesDecryptFast({ mode: "ecb", parameters }); return Utils.fromByteStringToArray(result); } const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [ "decrypt", ]); + + // CBC + if (iv == null) { + throw new Error("IV is required for CBC mode."); + } const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data); return new Uint8Array(buffer); } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts index 3c8c39e21e8..61f36fa0b75 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts @@ -8,7 +8,14 @@ import { DerivedStateProvider } from "../derived-state.provider"; import { DefaultDerivedState } from "./default-derived-state"; export class DefaultDerivedStateProvider implements DerivedStateProvider { - private cache: Record> = {}; + /** + * The cache uses a WeakMap to maintain separate derived states per user. + * Each user's state Observable acts as a unique key, without needing to + * pass around `userId`. Also, when a user's state Observable is cleaned up + * (like during an account swap) their cache is automatically garbage + * collected. + */ + private cache = new WeakMap, Record>>(); constructor() {} @@ -17,8 +24,14 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { + let stateCache = this.cache.get(parentState$); + if (!stateCache) { + stateCache = {}; + this.cache.set(parentState$, stateCache); + } + const cacheKey = deriveDefinition.buildCacheKey(); - const existingDerivedState = this.cache[cacheKey]; + const existingDerivedState = stateCache[cacheKey]; if (existingDerivedState != null) { // I have to cast out of the unknown generic but this should be safe if rules // around domain token are made @@ -26,7 +39,7 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { } const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies); - this.cache[cacheKey] = newDerivedState; + stateCache[cacheKey] = newDerivedState; return newDerivedState; } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts index 7e8d76bd203..6fcc1c408cb 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts @@ -9,6 +9,7 @@ import { DeriveDefinition } from "../derive-definition"; import { StateDefinition } from "../state-definition"; import { DefaultDerivedState } from "./default-derived-state"; +import { DefaultDerivedStateProvider } from "./default-derived-state.provider"; let callCount = 0; const cleanupDelayMs = 10; @@ -182,4 +183,29 @@ describe("DefaultDerivedState", () => { expect(await firstValueFrom(observable)).toEqual(new Date(newDate)); }); }); + + describe("account switching", () => { + let provider: DefaultDerivedStateProvider; + + beforeEach(() => { + provider = new DefaultDerivedStateProvider(); + }); + + it("should provide a dedicated cache for each account", async () => { + const user1State$ = new Subject(); + const user1Derived = provider.get(user1State$, deriveDefinition, deps); + const user1Emissions = trackEmissions(user1Derived.state$); + + const user2State$ = new Subject(); + const user2Derived = provider.get(user2State$, deriveDefinition, deps); + const user2Emissions = trackEmissions(user2Derived.state$); + + user1State$.next("2015-12-30"); + user2State$.next("2020-12-29"); + await awaitAsync(); + + expect(user1Emissions).toEqual([new Date("2015-12-30")]); + expect(user2Emissions).toEqual([new Date("2020-12-29")]); + }); + }); }); diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 1ae5b080360..c83119d9ad4 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -150,6 +150,9 @@ export const COLLECTION_DATA = new StateDefinition("collection", "disk", { web: "memory", }); export const FOLDER_DISK = new StateDefinition("folder", "disk", { web: "memory" }); +export const FOLDER_MEMORY = new StateDefinition("decryptedFolders", "memory", { + browser: "memory-large-object", +}); export const VAULT_FILTER_DISK = new StateDefinition("vaultFilter", "disk", { web: "disk-local", }); @@ -176,9 +179,12 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro web: "disk-local", }); export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk"); -export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk"); export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition( "newDeviceVerificationNotice", "disk", + { + web: "disk-local", + }, ); export const VAULT_APPEARANCE = new StateDefinition("vaultAppearance", "disk"); +export const SECURITY_TASKS_DISK = new StateDefinition("securityTasks", "disk"); diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 180767ccd16..cfa9030c9de 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -85,18 +85,25 @@ export abstract class CoreSyncService implements SyncService { await this.stateProvider.getUser(userId, LAST_SYNC_DATE).update(() => date); } - async syncUpsertFolder(notification: SyncFolderNotification, isEdit: boolean): Promise { + async syncUpsertFolder( + notification: SyncFolderNotification, + isEdit: boolean, + userId: UserId, + ): Promise { this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { + + const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); + + if (authStatus >= AuthenticationStatus.Locked) { try { - const localFolder = await this.folderService.get(notification.id); + const localFolder = await this.folderService.get(notification.id, userId); if ( (!isEdit && localFolder == null) || (isEdit && localFolder != null && localFolder.revisionDate < notification.revisionDate) ) { const remoteFolder = await this.folderApiService.get(notification.id); if (remoteFolder != null) { - await this.folderService.upsert(new FolderData(remoteFolder)); + await this.folderService.upsert(new FolderData(remoteFolder), userId); this.messageSender.send("syncedUpsertedFolder", { folderId: notification.id }); return this.syncCompleted(true); } @@ -108,10 +115,13 @@ export abstract class CoreSyncService implements SyncService { return this.syncCompleted(false); } - async syncDeleteFolder(notification: SyncFolderNotification): Promise { + async syncDeleteFolder(notification: SyncFolderNotification, userId: UserId): Promise { this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.folderService.delete(notification.id); + + const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); + + if (authStatus >= AuthenticationStatus.Locked) { + await this.folderService.delete(notification.id, userId); this.messageSender.send("syncedDeletedFolder", { folderId: notification.id }); this.syncCompleted(true); return true; diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 9e36aa69417..138c7c03318 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -8,8 +8,14 @@ import { CollectionDetailsResponse, } from "@bitwarden/admin-console/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LogoutReason } from "../../../../auth/src/common/types"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; diff --git a/libs/common/src/platform/sync/sync.service.ts b/libs/common/src/platform/sync/sync.service.ts index 733b7beaff5..6763e01cab7 100644 --- a/libs/common/src/platform/sync/sync.service.ts +++ b/libs/common/src/platform/sync/sync.service.ts @@ -56,8 +56,9 @@ export abstract class SyncService { abstract syncUpsertFolder( notification: SyncFolderNotification, isEdit: boolean, + userId: UserId, ): Promise; - abstract syncDeleteFolder(notification: SyncFolderNotification): Promise; + abstract syncDeleteFolder(notification: SyncFolderNotification, userId: UserId): Promise; abstract syncUpsertCipher( notification: SyncCipherNotification, isEdit: boolean, diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index dc0a8d61f64..f40745142d0 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -3,9 +3,9 @@ import { firstValueFrom } from "rxjs"; import { - CollectionRequest, CollectionAccessDetailsResponse, CollectionDetailsResponse, + CollectionRequest, CollectionResponse, } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; @@ -103,7 +103,6 @@ import { PaymentResponse } from "../billing/models/response/payment.response"; import { PlanResponse } from "../billing/models/response/plan.response"; import { SubscriptionResponse } from "../billing/models/response/subscription.response"; import { TaxInfoResponse } from "../billing/models/response/tax-info.response"; -import { TaxRateResponse } from "../billing/models/response/tax-rate.response"; import { DeviceType } from "../enums"; import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum"; import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request"; @@ -897,11 +896,6 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, PlanResponse); } - async getTaxRates(): Promise> { - const r = await this.send("GET", "/plans/sales-tax-rates/", null, true, true); - return new ListResponse(r, TaxRateResponse); - } - // Settings APIs async getSettingsDomains(): Promise { @@ -1835,7 +1829,7 @@ export class ApiService implements ApiServiceAbstraction { } async send( - method: "GET" | "POST" | "PUT" | "DELETE", + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", path: string, body: any, authed: boolean, @@ -1875,7 +1869,7 @@ export class ApiService implements ApiServiceAbstraction { return responseJson; } else if (hasResponse && response.status === 200 && responseIsCsv) { return await response.text(); - } else if (response.status !== 200) { + } else if (response.status !== 200 && response.status !== 204) { const error = await this.handleError(response, false, authed); return Promise.reject(error); } diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index 6f7c5c9f262..f88c904bee1 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -168,10 +168,14 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.syncService.syncUpsertFolder( notification.payload as SyncFolderNotification, notification.type === NotificationType.SyncFolderUpdate, + payloadUserId, ); break; case NotificationType.SyncFolderDelete: - await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification); + await this.syncService.syncDeleteFolder( + notification.payload as SyncFolderNotification, + payloadUserId, + ); break; case NotificationType.SyncVault: case NotificationType.SyncCiphers: @@ -223,6 +227,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.syncService.fullSync(true); } break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + if (isAuthenticated) { + await this.syncService.fullSync(true); + } + break; default: break; } diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 540f26bba2d..77ed6c960ab 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -6,10 +6,10 @@ import { FakeUserDecryptionOptions as UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -18,10 +18,12 @@ import { Policy } from "../../admin-console/models/domain/policy"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { LogService } from "../../platform/abstractions/log.service"; +import { Utils } from "../../platform/misc/utils"; import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION, } from "../../services/vault-timeout/vault-timeout-settings.state"; +import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index 0f5bdca93da..ffc8b6e0144 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -21,6 +21,8 @@ import { } from "@bitwarden/auth/common"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 71341a98a62..986bbbf95a4 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -3,8 +3,7 @@ import { BehaviorSubject, from, of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; +import { BiometricsService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; @@ -14,10 +13,12 @@ import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; +import { TaskSchedulerService } from "../../platform/scheduling"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; @@ -41,6 +42,7 @@ describe("VaultTimeoutService", () => { let stateEventRunnerService: MockProxy; let taskSchedulerService: MockProxy; let logService: MockProxy; + let biometricsService: MockProxy; let lockedCallback: jest.Mock, [userId: string]>; let loggedOutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: string]>; @@ -66,6 +68,7 @@ describe("VaultTimeoutService", () => { stateEventRunnerService = mock(); taskSchedulerService = mock(); logService = mock(); + biometricsService = mock(); lockedCallback = jest.fn(); loggedOutCallback = jest.fn(); @@ -93,6 +96,7 @@ describe("VaultTimeoutService", () => { stateEventRunnerService, taskSchedulerService, logService, + biometricsService, lockedCallback, loggedOutCallback, ); @@ -334,7 +338,7 @@ describe("VaultTimeoutService", () => { // Active users should have additional steps ran expect(searchService.clearIndex).toHaveBeenCalled(); - expect(folderService.clearCache).toHaveBeenCalled(); + expect(folderService.clearDecryptedFolderState).toHaveBeenCalled(); expectUserToHaveLoggedOut("3"); // They have chosen logout as their action and it's available, log them out expectUserToHaveLoggedOut("4"); // They may have had lock as their chosen action but it's not available to them so logout diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index f6ad0f17e9a..08dc02bb1ab 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -4,8 +4,7 @@ import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from " import { CollectionService } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { BiometricsService } from "@bitwarden/key-management"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -15,9 +14,11 @@ import { AuthService } from "../../auth/abstractions/auth.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "../../platform/scheduling"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; @@ -41,6 +42,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private stateEventRunnerService: StateEventRunnerService, private taskSchedulerService: TaskSchedulerService, protected logService: LogService, + private biometricService: BiometricsService, private lockedCallback: (userId?: string) => Promise = null, private loggedOutCallback: ( logoutReason: LogoutReason, @@ -98,6 +100,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } async lock(userId?: UserId): Promise { + await this.biometricService.setShouldAutopromptNow(false); + const authed = await this.stateService.getIsAuthenticated({ userId: userId }); if (!authed) { return; @@ -135,10 +139,10 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { if (userId == null || userId === currentUserId) { await this.searchService.clearIndex(); - await this.folderService.clearCache(); await this.collectionService.clearActiveUserCache(); } + await this.folderService.clearDecryptedFolderState(lockingUserId); await this.masterPasswordService.clearMasterKey(lockingUserId); await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId }); diff --git a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts index 53ae94c30b2..84bcaeec608 100644 --- a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts @@ -3,6 +3,8 @@ import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const AutofillOverlayVisibility = { Off: 0, OnButtonClick: 1, diff --git a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts index 04b4a50e0ed..9397191f54b 100644 --- a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts @@ -3,6 +3,8 @@ import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const ClearClipboardDelay = { Never: null as null, TenSeconds: 10, diff --git a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts index b465a69131a..71e5c531d50 100644 --- a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts @@ -3,6 +3,8 @@ import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const UriMatchStrategy = { Domain: 0, Host: 1, diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts index 12257905d1c..0b60aef4917 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts @@ -1,13 +1,13 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, Subject } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../types/csprng"; +import { OrganizationId, UserId } from "../../types/guid"; +import { OrgKey, UserKey } from "../../types/key"; import { OrganizationBound, UserBound } from "../dependencies"; import { KeyServiceLegacyEncryptorProvider } from "./key-service-legacy-encryptor-provider"; @@ -184,7 +184,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { singleUserId$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes when `userKey$` emits a falsy value after emitting a truthy value", () => { @@ -199,7 +199,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { userKey$.next(null); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes once `dependencies.singleUserId$` emits and `userKey$` completes", () => { @@ -214,7 +214,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { userKey$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); }); @@ -445,7 +445,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { singleOrganizationId$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes when `orgKeys$` emits a falsy value after emitting a truthy value", () => { @@ -466,7 +466,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { orgKey$.next(OrgRecords); orgKey$.next(null); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); it("completes once `dependencies.singleOrganizationId$` emits and `userKey$` completes", () => { @@ -486,7 +486,7 @@ describe("KeyServiceLegacyEncryptorProvider", () => { orgKey$.complete(); - expect(completed).toBeTrue(); + expect(completed).toBe(true); }); }); }); diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts index 3eee08eb6bd..d4a8dec7dc3 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts @@ -12,10 +12,10 @@ import { takeWhile, } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { OrganizationId, UserId } from "../../types/guid"; import { OrganizationBound, SingleOrganizationDependency, diff --git a/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts b/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts index dda8bfe957a..9eec207e92d 100644 --- a/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts +++ b/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { OrganizationId } from "@bitwarden/common/types/guid"; - import { EncString } from "../../platform/models/domain/enc-string"; +import { OrganizationId } from "../../types/guid"; /** An encryption strategy that protects a type's secrets with * organization-specific keys. This strategy is bound to a specific organization. diff --git a/libs/common/src/tools/cryptography/organization-key-encryptor.ts b/libs/common/src/tools/cryptography/organization-key-encryptor.ts index 3fdc0a1da32..d3b7dae10f5 100644 --- a/libs/common/src/tools/cryptography/organization-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/organization-key-encryptor.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { OrganizationId } from "@bitwarden/common/types/guid"; - import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { OrganizationId } from "../../types/guid"; import { OrgKey } from "../../types/key"; import { DataPacker } from "../state/data-packer.abstraction"; diff --git a/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts b/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts index d63a5e908ef..6bb0e252af3 100644 --- a/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts +++ b/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; /** An encryption strategy that protects a type's secrets with * user-specific keys. This strategy is bound to a specific user. diff --git a/libs/common/src/tools/cryptography/user-key-encryptor.ts b/libs/common/src/tools/cryptography/user-key-encryptor.ts index 15a3a1b6d5c..296c33ea1dc 100644 --- a/libs/common/src/tools/cryptography/user-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/user-key-encryptor.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; import { UserKey } from "../../types/key"; import { DataPacker } from "../state/data-packer.abstraction"; diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index cdae45bc94a..c22e71cff67 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { Policy } from "../admin-console/models/domain/policy"; +import { OrganizationId, UserId } from "../types/guid"; import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction"; import { UserEncryptor } from "./cryptography/user-encryptor.abstraction"; diff --git a/libs/common/src/tools/extension/data.ts b/libs/common/src/tools/extension/data.ts new file mode 100644 index 00000000000..cab6272a068 --- /dev/null +++ b/libs/common/src/tools/extension/data.ts @@ -0,0 +1,26 @@ +/** well-known name for a feature extensible through an extension. */ +export const Site = Object.freeze({ + forwarder: "forwarder", +} as const); + +/** well-known name for a field surfaced from an extension site to a vendor. */ +export const Field = Object.freeze({ + token: "token", + baseUrl: "baseUrl", + domain: "domain", + prefix: "prefix", +} as const); + +/** Permission levels for metadata. */ +export const Permission = Object.freeze({ + /** unless a rule denies access, allow it. If a permission is `null` + * or `undefined` it should be treated as `Permission.default`. + */ + default: "default", + /** unless a rule allows access, deny it. */ + none: "none", + /** access is explicitly granted to use an extension. */ + allow: "allow", + /** access is explicitly prohibited for this extension. This rule overrides allow rules. */ + deny: "deny", +} as const); diff --git a/libs/common/src/tools/extension/extension-registry.abstraction.ts b/libs/common/src/tools/extension/extension-registry.abstraction.ts new file mode 100644 index 00000000000..7734c01ea50 --- /dev/null +++ b/libs/common/src/tools/extension/extension-registry.abstraction.ts @@ -0,0 +1,104 @@ +import { ExtensionSite } from "./extension-site"; +import { + ExtensionMetadata, + ExtensionSet, + ExtensionPermission, + SiteId, + SiteMetadata, + VendorId, + VendorMetadata, +} from "./type"; + +/** Tracks extension sites and the vendors that extend them. */ +export abstract class ExtensionRegistry { + /** Registers a site supporting extensibility. + * Each site may only be registered once. Calls after the first for + * the same SiteId have no effect. + * @param site identifies the site being extended + * @param meta configures the extension site + * @return self for method chaining. + * @remarks The registry initializes with a set of allowed sites and fields. + * `registerSite` drops a registration and trims its allowed fields to only + * those indicated in the allow list. + */ + abstract registerSite: (meta: SiteMetadata) => this; + + /** List all registered extension sites with their extension permission, if any. + * @returns a list of all extension sites. `permission` is defined when the site + * is associated with an extension permission. + */ + abstract sites: () => { site: SiteMetadata; permission?: ExtensionPermission }[]; + + /** Get a site's metadata + * @param site identifies a site registration + * @return the site's metadata or `undefined` if the site isn't registered. + */ + abstract site: (site: SiteId) => SiteMetadata | undefined; + + /** Registers a vendor providing an extension. + * Each vendor may only be registered once. Calls after the first for + * the same VendorId have no effect. + * @param site - identifies the site being extended + * @param meta - configures the extension site + * @return self for method chaining. + */ + abstract registerVendor: (meta: VendorMetadata) => this; + + /** List all registered vendors with their permissions, if any. + * @returns a list of all extension sites. `permission` is defined when the site + * is associated with an extension permission. + */ + abstract vendors: () => { vendor: VendorMetadata; permission?: ExtensionPermission }[]; + + /** Get a vendor's metadata + * @param site identifies a vendor registration + * @return the vendor's metadata or `undefined` if the vendor isn't registered. + */ + abstract vendor: (vendor: VendorId) => VendorMetadata | undefined; + + /** Registers an extension provided by a vendor to an extension site. + * The vendor and site MUST be registered before the extension. + * Each extension may only be registered once. Calls after the first for + * the same SiteId and VendorId have no effect. + * @param site - identifies the site being extended + * @param meta - configures the extension site + * @return self for method chaining. + */ + abstract registerExtension: (meta: ExtensionMetadata) => this; + + /** Get an extensions metadata + * @param site identifies the extension's site + * @param vendor identifies the extension's vendor + * @return the extension's metadata or `undefined` if the extension isn't registered. + */ + abstract extension: (site: SiteId, vendor: VendorId) => ExtensionMetadata | undefined; + + /** List all registered extensions and their permissions */ + abstract extensions: () => ReadonlyArray<{ + extension: ExtensionMetadata; + permissions: ExtensionPermission[]; + }>; + + /** Registers a permission. Only 1 permission can be registered for each extension set. + * Calls after the first *replace* the registered permission. + * @param set the collection of extensions affected by the permission + * @param permission the permission for the collection + * @return self for method chaining. + */ + abstract setPermission: (set: ExtensionSet, permission: ExtensionPermission) => this; + + /** Retrieves the current permission for the given extension set or `undefined` if + * a permission doesn't exist. + */ + abstract permission: (set: ExtensionSet) => ExtensionPermission | undefined; + + /** Returns all registered extension rules. */ + abstract permissions: () => { set: ExtensionSet; permission: ExtensionPermission }[]; + + /** Creates a point-in-time snapshot of the registry's contents with extension + * permissions applied for the provided SiteId. + * @param id identifies the extension site to create. + * @returns the extension site, or `undefined` if the site is not registered. + */ + abstract build: (id: SiteId) => ExtensionSite | undefined; +} diff --git a/libs/common/src/tools/extension/extension-site.ts b/libs/common/src/tools/extension/extension-site.ts new file mode 100644 index 00000000000..e8aba008493 --- /dev/null +++ b/libs/common/src/tools/extension/extension-site.ts @@ -0,0 +1,20 @@ +import { deepFreeze } from "../util"; + +import { ExtensionMetadata, SiteMetadata, VendorId } from "./type"; + +/** Describes the capabilities of an extension site. + * This type is immutable. + */ +export class ExtensionSite { + /** instantiate the extension site + * @param site describes the extension site + * @param vendors describes the available vendors + * @param extensions describes the available extensions + */ + constructor( + readonly site: Readonly, + readonly extensions: ReadonlyMap>, + ) { + deepFreeze(this); + } +} diff --git a/libs/common/src/tools/extension/factory.ts b/libs/common/src/tools/extension/factory.ts new file mode 100644 index 00000000000..10ebc77804a --- /dev/null +++ b/libs/common/src/tools/extension/factory.ts @@ -0,0 +1,24 @@ +import { DefaultFields, DefaultSites, Extension } from "./metadata"; +import { RuntimeExtensionRegistry } from "./runtime-extension-registry"; +import { VendorExtensions, Vendors } from "./vendor"; + +// FIXME: find a better way to build the registry than a hard-coded factory function + +/** Constructs the extension registry */ +export function buildExtensionRegistry() { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + for (const site of Reflect.ownKeys(Extension) as string[]) { + registry.registerSite(Extension[site]); + } + + for (const vendor of Vendors) { + registry.registerVendor(vendor); + } + + for (const extension of VendorExtensions) { + registry.registerExtension(extension); + } + + return registry; +} diff --git a/libs/common/src/tools/extension/index.ts b/libs/common/src/tools/extension/index.ts new file mode 100644 index 00000000000..e786dde4f59 --- /dev/null +++ b/libs/common/src/tools/extension/index.ts @@ -0,0 +1,12 @@ +export { Site, Field, Permission } from "./data"; +export { + SiteId, + FieldId, + VendorId, + ExtensionId, + ExtensionPermission, + SiteMetadata, + ExtensionMetadata, + VendorMetadata, +} from "./type"; +export { ExtensionSite } from "./extension-site"; diff --git a/libs/common/src/tools/extension/metadata.ts b/libs/common/src/tools/extension/metadata.ts new file mode 100644 index 00000000000..895b1d1b31f --- /dev/null +++ b/libs/common/src/tools/extension/metadata.ts @@ -0,0 +1,17 @@ +import { Field, Site, Permission } from "./data"; +import { FieldId, SiteId, SiteMetadata } from "./type"; + +export const DefaultSites: SiteId[] = Object.freeze(Object.keys(Site) as any); + +export const DefaultFields: FieldId[] = Object.freeze(Object.keys(Field) as any); + +export const Extension: Record = { + [Site.forwarder]: { + id: Site.forwarder, + availableFields: [Field.baseUrl, Field.domain, Field.prefix, Field.token], + }, +}; + +export const AllowedPermissions: ReadonlyArray = Object.freeze( + Object.values(Permission), +); diff --git a/libs/common/src/tools/extension/runtime-extension-registry.spec.ts b/libs/common/src/tools/extension/runtime-extension-registry.spec.ts new file mode 100644 index 00000000000..6aa7382db57 --- /dev/null +++ b/libs/common/src/tools/extension/runtime-extension-registry.spec.ts @@ -0,0 +1,923 @@ +import { deepFreeze } from "../util"; + +import { Field, Site, Permission } from "./data"; +import { ExtensionSite } from "./extension-site"; +import { DefaultFields, DefaultSites } from "./metadata"; +import { RuntimeExtensionRegistry } from "./runtime-extension-registry"; +import { ExtensionMetadata, SiteId, SiteMetadata, VendorMetadata } from "./type"; +import { Bitwarden } from "./vendor/bitwarden"; + +// arbitrary test entities +const SomeSiteId: SiteId = Site.forwarder; + +const SomeSite: SiteMetadata = Object.freeze({ + id: SomeSiteId, + availableFields: [], +}); + +const SomeVendor = Bitwarden; +const SomeVendorId = SomeVendor.id; +const SomeExtension: ExtensionMetadata = deepFreeze({ + site: SomeSite, + product: { vendor: SomeVendor, name: "Some Product" }, + host: { authorization: "bearer", selfHost: "maybe", baseUrl: "https://vault.bitwarden.com" }, + requestedFields: [], +}); + +const JustTrustUs: VendorMetadata = Object.freeze({ + id: "justrustus" as any, + name: "JustTrust.Us", +}); +const JustTrustUsExtension: ExtensionMetadata = deepFreeze({ + site: SomeSite, + product: { vendor: JustTrustUs }, + host: { authorization: "bearer", selfHost: "maybe", baseUrl: "https://justrust.us" }, + requestedFields: [], +}); + +// In the following tests, not-null assertions (`!`) indicate that +// the returned object should never be null or undefined given +// the conditions defined within the test case +describe("RuntimeExtensionRegistry", () => { + describe("registerSite", () => { + it("registers an extension site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + + const result = registry.registerSite(SomeSite).site(SomeSiteId); + + expect(result).toEqual(SomeSite); + }); + + it("interns the site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + + const result = registry.registerSite(SomeSite).site(SomeSiteId); + + expect(result).not.toBe(SomeSite); + }); + + it("registers an extension site with fields", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + const site: SiteMetadata = { + ...SomeSite, + availableFields: [Field.baseUrl], + }; + + const result = registry.registerSite(site).site(SomeSiteId); + + expect(result).toEqual(site); + }); + + it("ignores unavailable sites", () => { + const registry = new RuntimeExtensionRegistry([], []); + const ignored: SiteMetadata = { + id: "an-unavailable-site" as any, + availableFields: [], + }; + + const result = registry.registerSite(ignored).sites(); + + expect(result).toEqual([]); + }); + + it("ignores duplicate registrations", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const ignored: SiteMetadata = { + ...SomeSite, + availableFields: [Field.token], + }; + + const result = registry.registerSite(SomeSite).registerSite(ignored).site(SomeSiteId); + + expect(result).toEqual(SomeSite); + }); + + it("ignores unknown available fields", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + const ignored: SiteMetadata = { + ...SomeSite, + availableFields: [SomeSite.availableFields, "ignored" as any], + }; + + const { availableFields } = registry.registerSite(ignored).site(SomeSiteId)!; + + expect(availableFields).toEqual(SomeSite.availableFields); + }); + + it("freezes the site definition", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const site = registry.registerSite(SomeSite).site(SomeSiteId)!; + + // reassigning `availableFields` throws b/c the object is frozen + expect(() => (site.availableFields = [Field.domain])).toThrow(); + }); + }); + + describe("site", () => { + it("returns `undefined` for an unknown site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry.site(SomeSiteId); + + expect(result).toBeUndefined(); + }); + + it("returns the same result when called repeatedly", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerSite(SomeSite); + + const first = registry.site(SomeSiteId); + const second = registry.site(SomeSiteId); + + expect(first).toBe(second); + }); + }); + + describe("sites", () => { + it("lists registered sites", () => { + const registry = new RuntimeExtensionRegistry([SomeSiteId, "bar"] as any[], DefaultFields); + const barSite: SiteMetadata = { + id: "bar" as any, + availableFields: [], + }; + + const result = registry.registerSite(SomeSite).registerSite(barSite).sites(); + + expect(result.some(({ site }) => site.id === SomeSiteId)).toBe(true); + expect(result.some(({ site }) => site.id === barSite.id)).toBe(true); + }); + + it("includes permissions for a site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry + .registerSite(SomeSite) + .setPermission({ site: SomeSite.id }, Permission.allow) + .sites(); + + expect(result).toEqual([{ site: SomeSite, permission: Permission.allow }]); + }); + + it("ignores duplicate registrations", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + const ignored: SiteMetadata = { + ...SomeSite, + availableFields: [Field.token], + }; + + const result = registry.registerSite(SomeSite).registerSite(ignored).sites(); + + expect(result).toEqual([{ site: SomeSite }]); + }); + + it("ignores permissions for other sites", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry + .registerSite(SomeSite) + .setPermission({ site: SomeSite.id }, Permission.allow) + .setPermission({ site: "bar" as any }, Permission.deny) + .sites(); + + expect(result).toEqual([{ site: SomeSite, permission: Permission.allow }]); + }); + }); + + describe("registerVendor", () => { + it("registers a vendor", () => { + const registry = new RuntimeExtensionRegistry([], []); + const result = registry.registerVendor(SomeVendor).vendors(); + + expect(result).toEqual([{ vendor: SomeVendor }]); + }); + + it("freezes the vendor definition", () => { + const registry = new RuntimeExtensionRegistry([], []); + // copy `SomeVendor` because it is already frozen + const original: VendorMetadata = { ...SomeVendor }; + + const [{ vendor }] = registry.registerVendor(original).vendors(); + + // reassigning `name` throws b/c the object is frozen + expect(() => (vendor.name = "Bytewarden")).toThrow(); + }); + }); + + describe("vendor", () => { + it("returns `undefined` for an unknown site", () => { + const registry = new RuntimeExtensionRegistry([], []); + + const result = registry.vendor(SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("returns the same result when called repeatedly", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerVendor(SomeVendor); + + const first = registry.vendor(SomeVendorId); + const second = registry.vendor(SomeVendorId); + + expect(first).toBe(second); + }); + }); + + describe("vendors", () => { + it("lists registered vendors", () => { + const registry = new RuntimeExtensionRegistry([], []); + registry.registerVendor(SomeVendor).registerVendor(JustTrustUs); + + const result = registry.vendors(); + + expect(result.some(({ vendor }) => vendor.id === SomeVendorId)).toBe(true); + expect(result.some(({ vendor }) => vendor.id === JustTrustUs.id)).toBe(true); + }); + + it("includes permissions for a vendor", () => { + const registry = new RuntimeExtensionRegistry([], []); + + const result = registry + .registerVendor(SomeVendor) + .setPermission({ vendor: SomeVendorId }, Permission.allow) + .vendors(); + + expect(result).toEqual([{ vendor: SomeVendor, permission: Permission.allow }]); + }); + + it("ignores duplicate registrations", () => { + const registry = new RuntimeExtensionRegistry([], []); + const vendor: VendorMetadata = SomeVendor; + const ignored: VendorMetadata = { + ...SomeVendor, + name: "Duplicate", + }; + + const result = registry.registerVendor(vendor).registerVendor(ignored).vendors(); + + expect(result).toEqual([{ vendor }]); + }); + + it("ignores permissions for other sites", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerVendor(SomeVendor).setPermission({ vendor: SomeVendorId }, Permission.allow); + + const result = registry.setPermission({ vendor: JustTrustUs.id }, Permission.deny).vendors(); + + expect(result).toEqual([{ vendor: SomeVendor, permission: Permission.allow }]); + }); + }); + + describe("setPermission", () => { + it("sets the all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { all: true } as const; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("sets a vendor permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { vendor: SomeVendorId }; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("sets a site permission", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const target = { site: SomeSiteId }; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("ignores a site permission unless it is in the allowed sites list", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { site: SomeSiteId }; + + const permission = registry.setPermission(target, Permission.allow).permission(target); + + expect(permission).toBeUndefined(); + }); + + it("throws when a permission is invalid", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + + expect(() => registry.setPermission({ all: true }, "invalid" as any)).toThrow(); + }); + + it("throws when the extension set is the wrong type", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { invalid: "invalid" } as any; + + expect(() => registry.setPermission(target, Permission.allow)).toThrow(); + }); + }); + + describe("permission", () => { + it("gets the default all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { all: true } as const; + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.default); + }); + + it("gets an all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { all: true } as const; + registry.setPermission(target, Permission.none); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.none); + }); + + it("gets a vendor permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { vendor: SomeVendorId }; + registry.setPermission(target, Permission.allow); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("gets a site permission", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + const target = { site: SomeSiteId }; + registry.setPermission(target, Permission.allow); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("gets a vendor permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { vendor: SomeVendorId }; + registry.setPermission(target, Permission.allow); + + const permission = registry.permission(target); + + expect(permission).toEqual(Permission.allow); + }); + + it("returns undefined when the extension set is the wrong type", () => { + const registry = new RuntimeExtensionRegistry([], []); + const target = { invalid: "invalid" } as any; + + const permission = registry.permission(target); + + expect(permission).toBeUndefined(); + }); + }); + + describe("permissions", () => { + it("returns a default all permission by default", () => { + const registry = new RuntimeExtensionRegistry([], []); + + const permission = registry.permissions(); + + expect(permission).toEqual([{ set: { all: true }, permission: Permission.default }]); + }); + + it("returns the all permission", () => { + const registry = new RuntimeExtensionRegistry([], []); + registry.setPermission({ all: true }, Permission.none); + + const permission = registry.permissions(); + + expect(permission).toEqual([{ set: { all: true }, permission: Permission.none }]); + }); + + it("includes site permissions", () => { + const registry = new RuntimeExtensionRegistry([SomeSiteId, "bar"] as any[], DefaultFields); + registry.registerSite(SomeSite).setPermission({ site: SomeSiteId }, Permission.allow); + registry + .registerSite({ + id: "bar" as any, + availableFields: [], + }) + .setPermission({ site: "bar" as any }, Permission.deny); + + const result = registry.permissions(); + + expect( + result.some((p: any) => p.set.site === SomeSiteId && p.permission === Permission.allow), + ).toBe(true); + expect( + result.some((p: any) => p.set.site === "bar" && p.permission === Permission.deny), + ).toBe(true); + }); + + it("includes vendor permissions", () => { + const registry = new RuntimeExtensionRegistry([], DefaultFields); + registry.registerVendor(SomeVendor).setPermission({ vendor: SomeVendorId }, Permission.allow); + registry + .registerVendor(JustTrustUs) + .setPermission({ vendor: JustTrustUs.id }, Permission.deny); + + const result = registry.permissions(); + + expect( + result.some((p: any) => p.set.vendor === SomeVendorId && p.permission === Permission.allow), + ).toBe(true); + expect( + result.some( + (p: any) => p.set.vendor === JustTrustUs.id && p.permission === Permission.deny, + ), + ).toBe(true); + }); + }); + + describe("registerExtension", () => { + it("registers an extension", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId); + + expect(result).toEqual(SomeExtension); + }); + + it("ignores extensions with nonregistered sites", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerVendor(SomeVendor); + + // precondition: the site is not registered + expect(registry.site(SomeSiteId)).toBeUndefined(); + + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("ignores extensions with nonregistered vendors", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite); + + // precondition: the vendor is not registered + expect(registry.vendor(SomeVendorId)).toBeUndefined(); + + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("ignores repeated extensions with nonregistered vendors", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + + // precondition: the vendor is already registered + expect(registry.extension(SomeSiteId, SomeVendorId)).toBeDefined(); + + const result = registry + .registerExtension({ + ...SomeExtension, + requestedFields: [Field.domain], + }) + .extension(SomeSiteId, SomeVendorId); + + expect(result).toEqual(SomeExtension); + }); + + it("interns site metadata", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const internedSite = registry.site(SomeSiteId); + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId)!; + + expect(result.site).toBe(internedSite); + }); + + it("interns vendor metadata", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const internedVendor = registry.vendor(SomeVendorId); + const result = registry.registerExtension(SomeExtension).extension(SomeSiteId, SomeVendorId)!; + + expect(result.product.vendor).toBe(internedVendor); + }); + + it("freezes the extension metadata", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + const extension = registry.extension(SomeSiteId, SomeVendorId)!; + + // field assignments & mutation functions throw b/c the object is frozen + expect(() => ((extension.site as any) = SomeSite)).toThrow(); + expect(() => ((extension.product.vendor as any) = SomeVendor)).toThrow(); + expect(() => ((extension.product.name as any) = "SomeVendor")).toThrow(); + expect(() => ((extension.host as any) = {})).toThrow(); + expect(() => ((extension.host.selfHost as any) = {})).toThrow(); + expect(() => ((extension.host as any).authorization = "basic")).toThrow(); + expect(() => ((extension.host as any).baseUrl = "https://www.example.com")).toThrow(); + expect(() => ((extension.requestedFields as any) = [Field.baseUrl])).toThrow(); + expect(() => (extension.requestedFields as any).push(Field.baseUrl)).toThrow(); + }); + }); + + describe("extension", () => { + describe("extension", () => { + it("returns `undefined` for an unknown extension", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry.extension(SomeSiteId, SomeVendorId); + + expect(result).toBeUndefined(); + }); + + it("interns the extension", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + + const first = registry.extension(SomeSiteId, SomeVendorId); + const second = registry.extension(SomeSiteId, SomeVendorId); + + expect(first).toBe(second); + }); + }); + + describe("extensions", () => { + it("lists registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry.registerSite(SomeSite); + registry.registerVendor(SomeVendor).registerExtension(SomeExtension); + registry.registerVendor(JustTrustUs).registerExtension(JustTrustUsExtension); + + const result = registry.extensions(); + + expect( + result.some( + ({ extension }) => + extension.site.id === SomeSiteId && extension.product.vendor.id === SomeVendorId, + ), + ).toBe(true); + expect( + result.some( + ({ extension }) => + extension.site.id === SomeSiteId && extension.product.vendor.id === JustTrustUs.id, + ), + ).toBe(true); + }); + + it("includes permissions for extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ vendor: SomeVendorId }, Permission.allow); + + const result = registry.extensions(); + + expect( + result.some( + ({ extension, permissions }) => + extension.site.id === SomeSiteId && + extension.product.vendor.id === SomeVendorId && + permissions.includes(Permission.allow), + ), + ).toBe(true); + }); + }); + + describe("build", () => { + it("builds an empty extension site when no extensions are registered", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }); + + it("builds an extension site with all registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry.registerSite(SomeSite).registerVendor(SomeVendor).registerExtension(SomeExtension); + const expected = registry.extension(SomeSiteId, SomeVendorId); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.get(SomeVendorId)).toBe(expected); + }); + + it("returns `undefined` for an unknown site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + + const result = registry.build(SomeSiteId); + + expect(result).toBeUndefined(); + }); + + describe("when the all permission is `default`", () => { + const allPermission = Permission.default; + + it("builds an extension site with all registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.default); + const expected = registry.extension(SomeSiteId, SomeVendorId); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toBe(expected); + }); + + it.each([[Permission.default], [Permission.allow]])( + "includes sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.none], [Permission.deny]])( + "ignores sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + + it.each([[Permission.default], [Permission.allow]])( + "includes vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.none], [Permission.deny]])( + "ignores vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + }); + + describe("when the all permission is `none`", () => { + const allPermission = Permission.none; + + it("builds an empty extension site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.none); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.size).toBe(0); + }); + + it.each([[Permission.allow]])("includes sites with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }); + + it.each([[Permission.default], [Permission.none], [Permission.deny]])( + "ignores sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + + it.each([[Permission.allow]])("includes vendors with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }); + + it.each([[Permission.default], [Permission.none], [Permission.deny]])( + "ignores vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + }); + + describe("when the all permission is `allow`", () => { + const allPermission = Permission.allow; + + it("builds an extension site with all registered extensions", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.default); + const expected = registry.extension(SomeSiteId, SomeVendorId); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.get(SomeVendorId)).toBe(expected); + }); + + it.each([[Permission.default], [Permission.none], [Permission.allow]])( + "includes sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.deny]])("ignores sites with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }); + + it.each([[Permission.default], [Permission.none], [Permission.allow]])( + "includes vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.get(SomeVendorId)).toEqual(SomeExtension); + }, + ); + + it.each([[Permission.deny]])("ignores vendors with `%p` permission", (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }); + }); + + describe("when the all permission is `deny`", () => { + const allPermission = Permission.deny; + + it("builds an empty extension site", () => { + const registry = new RuntimeExtensionRegistry(DefaultSites, DefaultFields); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension) + .setPermission({ all: true }, Permission.deny); + + const result = registry.build(SomeSiteId)!; + + expect(result).toBeInstanceOf(ExtensionSite); + expect(result.extensions.size).toBe(0); + }); + + it.each([[Permission.default], [Permission.none], [Permission.allow], [Permission.deny]])( + "ignores sites with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ site: SomeSiteId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + + it.each([[Permission.default], [Permission.none], [Permission.allow], [Permission.deny]])( + "ignores vendors with `%p` permission", + (permission) => { + const registry = new RuntimeExtensionRegistry(DefaultSites, []); + registry + .registerSite(SomeSite) + .registerVendor(SomeVendor) + .registerExtension(SomeExtension); + registry.setPermission({ all: true }, allPermission); + registry.setPermission({ vendor: SomeVendorId }, permission); + + const result = registry.build(SomeSiteId)!; + + expect(result.extensions.size).toBe(0); + }, + ); + }); + }); + }); +}); diff --git a/libs/common/src/tools/extension/runtime-extension-registry.ts b/libs/common/src/tools/extension/runtime-extension-registry.ts new file mode 100644 index 00000000000..1c630dcc915 --- /dev/null +++ b/libs/common/src/tools/extension/runtime-extension-registry.ts @@ -0,0 +1,286 @@ +import { deepFreeze } from "../util"; + +import { ExtensionRegistry } from "./extension-registry.abstraction"; +import { ExtensionSite } from "./extension-site"; +import { AllowedPermissions } from "./metadata"; +import { + ExtensionMetadata, + ExtensionPermission, + ExtensionSet, + FieldId, + ProductMetadata, + SiteMetadata, + SiteId, + VendorId, + VendorMetadata, +} from "./type"; + +/** Tracks extension sites and the vendors that extend them in application memory. */ +export class RuntimeExtensionRegistry implements ExtensionRegistry { + /** Instantiates the extension registry + * @param allowedSites sites that are valid for use by any extension; + * this is most useful to disable an extension site that is only + * available on a specific client. + * @param allowedFields fields that are valid for use by any extension; + * this is most useful to prohibit access to a field via policy. + */ + constructor( + private readonly allowedSites: SiteId[], + private readonly allowedFields: FieldId[], + ) { + Object.freeze(this.allowedFields); + Object.freeze(this.allowedSites); + } + + private allPermission: ExtensionPermission = "default"; + + private siteRegistrations = new Map(); + private sitePermissions = new Map(); + + private vendorRegistrations = new Map(); + private vendorPermissions = new Map(); + + private extensionRegistrations = new Array(); + private extensionsBySiteByVendor = new Map>(); + + registerSite(site: SiteMetadata): this { + if (!this.allowedSites.includes(site.id)) { + return this; + } + + // verify requested fields are on the list of valid fields to expose to + // an extension + const availableFields = site.availableFields.filter((field) => + this.allowedFields.includes(field), + ); + const validated: SiteMetadata = deepFreeze({ id: site.id, availableFields }); + + if (!this.siteRegistrations.has(site.id)) { + this.siteRegistrations.set(site.id, validated); + } + + return this; + } + + site(site: SiteId): SiteMetadata | undefined { + const result = this.siteRegistrations.get(site); + return result; + } + + sites() { + const sites: { site: SiteMetadata; permission?: ExtensionPermission }[] = []; + + for (const [k, site] of this.siteRegistrations.entries()) { + const s: (typeof sites)[number] = { site }; + const permission = this.sitePermissions.get(k); + if (permission) { + s.permission = permission; + } + + sites.push(s); + } + + return sites; + } + + registerVendor(vendor: VendorMetadata): this { + if (!this.vendorRegistrations.has(vendor.id)) { + const frozen = deepFreeze(vendor); + this.vendorRegistrations.set(vendor.id, frozen); + } + + return this; + } + + vendor(vendor: VendorId): VendorMetadata | undefined { + const result = this.vendorRegistrations.get(vendor); + return result; + } + + vendors() { + const vendors: { vendor: VendorMetadata; permission?: ExtensionPermission }[] = []; + + for (const [k, vendor] of this.vendorRegistrations.entries()) { + const s: (typeof vendors)[number] = { vendor }; + const permission = this.vendorPermissions.get(k); + if (permission) { + s.permission = permission; + } + + vendors.push(s); + } + + return vendors; + } + + setPermission(set: ExtensionSet, permission: ExtensionPermission): this { + if (!AllowedPermissions.includes(permission)) { + throw new Error(`invalid extension permission: ${permission}`); + } + + if ("all" in set && set.all) { + this.allPermission = permission; + } else if ("vendor" in set) { + this.vendorPermissions.set(set.vendor, permission); + } else if ("site" in set) { + if (this.allowedSites.includes(set.site)) { + this.sitePermissions.set(set.site, permission); + } + } else { + throw new Error(`Unrecognized extension set received: ${JSON.stringify(set)}.`); + } + + return this; + } + + permission(set: ExtensionSet) { + if ("all" in set && set.all) { + return this.allPermission; + } else if ("vendor" in set) { + return this.vendorPermissions.get(set.vendor); + } else if ("site" in set) { + return this.sitePermissions.get(set.site); + } else { + return undefined; + } + } + + permissions() { + const rules: { set: ExtensionSet; permission: ExtensionPermission }[] = []; + rules.push({ set: { all: true }, permission: this.allPermission }); + + for (const [site, permission] of this.sitePermissions.entries()) { + rules.push({ set: { site }, permission }); + } + + for (const [vendor, permission] of this.vendorPermissions.entries()) { + rules.push({ set: { vendor }, permission }); + } + + return rules; + } + + registerExtension(meta: ExtensionMetadata): this { + const site = this.siteRegistrations.get(meta.site.id); + const vendor = this.vendorRegistrations.get(meta.product.vendor.id); + if (!site || !vendor) { + return this; + } + + // exit early if the extension is already registered + const extensionsByVendor = + this.extensionsBySiteByVendor.get(meta.site.id) ?? new Map(); + if (extensionsByVendor.has(meta.product.vendor.id)) { + return this; + } + + // create immutable copy; this updates the vendor and site with + // their internalized representation to provide reference equality + // across registrations + const product: ProductMetadata = { vendor }; + if (meta.product.name) { + product.name = meta.product.name; + } + const extension: ExtensionMetadata = Object.freeze({ + site, + product: Object.freeze(product), + host: Object.freeze({ ...meta.host }), + requestedFields: Object.freeze([...meta.requestedFields]), + }); + + // register it + const index = this.extensionRegistrations.push(extension) - 1; + extensionsByVendor.set(vendor.id, index); + this.extensionsBySiteByVendor.set(site.id, extensionsByVendor); + + return this; + } + + extension(site: SiteId, vendor: VendorId): ExtensionMetadata | undefined { + const index = this.extensionsBySiteByVendor.get(site)?.get(vendor) ?? -1; + if (index < 0) { + return undefined; + } else { + return this.extensionRegistrations[index]; + } + } + + private getPermissions(site: SiteId, vendor: VendorId): ExtensionPermission[] { + const permissions = [ + this.sitePermissions.get(site), + this.vendorPermissions.get(vendor), + this.allPermission, + // Need to cast away `undefined` because typescript isn't + // aware that the filter eliminates undefined elements + ].filter((p) => !!p) as ExtensionPermission[]; + + return permissions; + } + + extensions(): ReadonlyArray<{ + extension: ExtensionMetadata; + permissions: ExtensionPermission[]; + }> { + const extensions = []; + for (const extension of this.extensionRegistrations) { + const permissions = this.getPermissions(extension.site.id, extension.product.vendor.id); + + extensions.push({ extension, permissions }); + } + + return extensions; + } + + build(id: SiteId): ExtensionSite | undefined { + const site = this.siteRegistrations.get(id); + if (!site) { + return undefined; + } + + if (this.allPermission === "deny") { + return new ExtensionSite(site, new Map()); + } + + const extensions = new Map(); + const entries = this.extensionsBySiteByVendor.get(id)?.entries() ?? ([] as const); + for (const [vendor, index] of entries) { + const permissions = this.getPermissions(id, vendor); + + const extension = evaluate(permissions, this.extensionRegistrations[index]); + if (extension) { + extensions.set(vendor, extension); + } + } + + const extensionSite = new ExtensionSite(site, extensions); + return extensionSite; + } +} + +function evaluate( + permissions: ExtensionPermission[], + value: ExtensionMetadata, +): ExtensionMetadata | undefined { + // deny always wins + if (permissions.includes("deny")) { + return undefined; + } + + // allow overrides implicit permissions + if (permissions.includes("allow")) { + return value; + } + + // none permission becomes a deny + if (permissions.includes("none")) { + return undefined; + } + + // default permission becomes an allow + if (permissions.includes("default")) { + return value; + } + + // if no permission is recognized, throw. This code is unreachable. + throw new Error("failed to recognize any permissions"); +} diff --git a/libs/common/src/tools/extension/type.ts b/libs/common/src/tools/extension/type.ts new file mode 100644 index 00000000000..f37d4ff8e53 --- /dev/null +++ b/libs/common/src/tools/extension/type.ts @@ -0,0 +1,109 @@ +import { Opaque } from "type-fest"; + +import { Site, Field, Permission } from "./data"; + +/** well-known name for a feature extensible through an extension. */ +export type SiteId = keyof typeof Site; + +/** well-known name for a field surfaced from an extension site to a vendor. */ +export type FieldId = keyof typeof Field; + +/** Identifies a vendor extending bitwarden */ +export type VendorId = Opaque<"vendor", string>; + +/** uniquely identifies an extension. */ +export type ExtensionId = { site: SiteId; vendor: VendorId }; + +/** Permission levels for metadata. */ +export type ExtensionPermission = keyof typeof Permission; + +/** The capabilities and descriptive content for an extension */ +export type SiteMetadata = { + /** Uniquely identifies the extension site. */ + id: SiteId; + + /** Lists the fields disclosed by the extension to the vendor */ + availableFields: FieldId[]; +}; + +/** The capabilities and descriptive content for an extension */ +export type VendorMetadata = { + /** Uniquely identifies the vendor. */ + id: VendorId; + + /** Brand name of the service providing the extension. */ + name: string; +}; + +type TokenHeader = + | { + /** Transmit the token as the value of an `Authentication` header */ + authentication: true; + } + | { + /** Transmit the token as an `Authorization` header and a formatted value + * * `bearer` uses OAUTH-2.0 bearer token format + * * `token` prefixes the token with "Token" + * * `basic-username` uses HTTP Basic authentication format, encoding the + * token as the username. + */ + authorization: "bearer" | "token" | "basic-username"; + }; + +/** Catalogues an extension's hosting status. + * selfHost: "never" always uses the service's base URL + * selfHost: "maybe" allows the user to override the service's + * base URL with their own. + * selfHost: "always" requires a base URL. + */ +export type ApiHost = TokenHeader & + ( + | { selfHost: "never"; baseUrl: string } + | { selfHost: "maybe"; baseUrl: string } + | { selfHost: "always" } + ); + +/** Describes a branded product */ +export type ProductMetadata = { + /** The vendor providing the extension */ + vendor: VendorMetadata; + + /** The branded name of the product, if it varies from the Vendor name */ + name?: string; +}; + +/** Describes an extension provided by a vendor */ +export type ExtensionMetadata = { + /** The part of Bitwarden extended by the vendor's services */ + readonly site: Readonly; + + /** Product description */ + readonly product: Readonly; + + /** Hosting provider capabilities required by the extension */ + readonly host: Readonly; + + /** Lists the fields disclosed by the extension to the vendor. + * This should be a subset of the `availableFields` listed in + * the extension. + */ + readonly requestedFields: ReadonlyArray>; +}; + +/** Identifies a collection of extensions. + */ +export type ExtensionSet = + | { + /** A set of extensions sharing an extension point */ + site: SiteId; + } + | { + /** A set of extensions sharing a vendor */ + vendor: VendorId; + } + | { + /** The total set of extensions. This is used to set a categorical + * rule affecting all extensions. + */ + all: true; + }; diff --git a/libs/common/src/tools/extension/vendor/addyio.ts b/libs/common/src/tools/extension/vendor/addyio.ts new file mode 100644 index 00000000000..c33abd570ad --- /dev/null +++ b/libs/common/src/tools/extension/vendor/addyio.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const AddyIo: VendorMetadata = { + id: Vendor.addyio, + name: "Addy.io", +}; + +export const AddyIoExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: AddyIo, + }, + host: { + authorization: "bearer", + selfHost: "maybe", + baseUrl: "https://app.addy.io", + }, + requestedFields: [Field.token, Field.baseUrl, Field.domain], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/bitwarden.ts b/libs/common/src/tools/extension/vendor/bitwarden.ts new file mode 100644 index 00000000000..7f659c2d07f --- /dev/null +++ b/libs/common/src/tools/extension/vendor/bitwarden.ts @@ -0,0 +1,8 @@ +import { VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const Bitwarden: VendorMetadata = Object.freeze({ + id: Vendor.bitwarden, + name: "Bitwarden", +}); diff --git a/libs/common/src/tools/extension/vendor/data.ts b/libs/common/src/tools/extension/vendor/data.ts new file mode 100644 index 00000000000..7f0802ef82f --- /dev/null +++ b/libs/common/src/tools/extension/vendor/data.ts @@ -0,0 +1,11 @@ +import { VendorId } from "../type"; + +export const Vendor = Object.freeze({ + addyio: "addyio" as VendorId, + bitwarden: "bitwarden" as VendorId, // RESERVED + duckduckgo: "duckduckgo" as VendorId, + fastmail: "fastmail" as VendorId, + forwardemail: "forwardemail" as VendorId, + mozilla: "mozilla" as VendorId, + simplelogin: "simplelogin" as VendorId, +} as const); diff --git a/libs/common/src/tools/extension/vendor/duckduckgo.ts b/libs/common/src/tools/extension/vendor/duckduckgo.ts new file mode 100644 index 00000000000..ca4634192f5 --- /dev/null +++ b/libs/common/src/tools/extension/vendor/duckduckgo.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const DuckDuckGo: VendorMetadata = { + id: Vendor.duckduckgo, + name: "DuckDuckGo", +}; + +export const DuckDuckGoExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: DuckDuckGo, + }, + host: { + authorization: "bearer", + selfHost: "never", + baseUrl: "https://quack.duckduckgo.com/api", + }, + requestedFields: [Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/fastmail.ts b/libs/common/src/tools/extension/vendor/fastmail.ts new file mode 100644 index 00000000000..e6fb9ec16be --- /dev/null +++ b/libs/common/src/tools/extension/vendor/fastmail.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const Fastmail: VendorMetadata = { + id: Vendor.fastmail, + name: "Fastmail", +}; + +export const FastmailExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: Fastmail, + }, + host: { + authorization: "bearer", + selfHost: "maybe", + baseUrl: "https://api.fastmail.com", + }, + requestedFields: [Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/forwardemail.ts b/libs/common/src/tools/extension/vendor/forwardemail.ts new file mode 100644 index 00000000000..4fbc8c139b1 --- /dev/null +++ b/libs/common/src/tools/extension/vendor/forwardemail.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const ForwardEmail: VendorMetadata = { + id: Vendor.forwardemail, + name: "Forward Email", +}; + +export const ForwardEmailExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: ForwardEmail, + }, + host: { + authorization: "basic-username", + selfHost: "never", + baseUrl: "https://api.forwardemail.net", + }, + requestedFields: [Field.domain, Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/index.ts b/libs/common/src/tools/extension/vendor/index.ts new file mode 100644 index 00000000000..3bac78c80db --- /dev/null +++ b/libs/common/src/tools/extension/vendor/index.ts @@ -0,0 +1,30 @@ +import { deepFreeze } from "../../util"; + +import { AddyIo, AddyIoExtensions } from "./addyio"; +import { Bitwarden } from "./bitwarden"; +import { DuckDuckGo, DuckDuckGoExtensions } from "./duckduckgo"; +import { Fastmail, FastmailExtensions } from "./fastmail"; +import { ForwardEmail, ForwardEmailExtensions } from "./forwardemail"; +import { Mozilla, MozillaExtensions } from "./mozilla"; +import { SimpleLogin, SimpleLoginExtensions } from "./simplelogin"; + +export const Vendors = deepFreeze([ + AddyIo, + Bitwarden, + DuckDuckGo, + Fastmail, + ForwardEmail, + Mozilla, + SimpleLogin, +]); + +export const VendorExtensions = deepFreeze( + [ + AddyIoExtensions, + DuckDuckGoExtensions, + FastmailExtensions, + ForwardEmailExtensions, + MozillaExtensions, + SimpleLoginExtensions, + ].flat(), +); diff --git a/libs/common/src/tools/extension/vendor/mozilla.ts b/libs/common/src/tools/extension/vendor/mozilla.ts new file mode 100644 index 00000000000..b02b97d8777 --- /dev/null +++ b/libs/common/src/tools/extension/vendor/mozilla.ts @@ -0,0 +1,26 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const Mozilla: VendorMetadata = { + id: Vendor.mozilla, + name: "Mozilla", +}; + +export const MozillaExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: Mozilla, + name: "Firefox Relay", + }, + host: { + authorization: "token", + selfHost: "never", + baseUrl: "https://relay.firefox.com/api", + }, + requestedFields: [Field.token], + }, +]; diff --git a/libs/common/src/tools/extension/vendor/readme.md b/libs/common/src/tools/extension/vendor/readme.md new file mode 100644 index 00000000000..507769edd4e --- /dev/null +++ b/libs/common/src/tools/extension/vendor/readme.md @@ -0,0 +1,33 @@ +# Vendors + +This folder contains vendor-specific logic that extends the +Bitwarden password manager. + +## Vendor IDs + +A vendor's ID is used to identify and trace the code provided by +a vendor across Bitwarden. There are a few rules that vendor ids +must follow: + +1. They should be human-readable. (No UUIDs.) +2. They may only contain lowercase ASCII characters and numbers. +3. They must retain backwards compatibility with prior versions. + +As such, any given ID may not not match the vendor's present +brand identity. Said branding may be stored in `VendorMetadata.name`. + +## Core files + +There are 4 vendor-independent files in this directory. + +- `data.ts` - core metadata used for system initialization +- `index.ts` - exports vendor metadata +- `README.md` - this file + +## Vendor definitions + +Each vendor should have one and only one definition, whose name +MUST match their `VendorId`. The vendor is free to use either a +single file (e.g. `bitwarden.ts`) or a folder containing multiple +files (e.g. `bitwarden/extension.ts`, `bitwarden/forwarder.ts`) to +host their files. diff --git a/libs/common/src/tools/extension/vendor/simplelogin.ts b/libs/common/src/tools/extension/vendor/simplelogin.ts new file mode 100644 index 00000000000..21ee969cebb --- /dev/null +++ b/libs/common/src/tools/extension/vendor/simplelogin.ts @@ -0,0 +1,25 @@ +import { Field } from "../data"; +import { Extension } from "../metadata"; +import { ExtensionMetadata, VendorMetadata } from "../type"; + +import { Vendor } from "./data"; + +export const SimpleLogin: VendorMetadata = { + id: Vendor.simplelogin, + name: "SimpleLogin", +}; + +export const SimpleLoginExtensions: ExtensionMetadata[] = [ + { + site: Extension.forwarder, + product: { + vendor: SimpleLogin, + }, + host: { + authentication: true, + selfHost: "maybe", + baseUrl: "https://app.simplelogin.io", + }, + requestedFields: [Field.baseUrl, Field.token, Field.domain], + }, +]; diff --git a/libs/common/src/tools/integration/integration-context.spec.ts b/libs/common/src/tools/integration/integration-context.spec.ts index 58115c783c7..42581c08dee 100644 --- a/libs/common/src/tools/integration/integration-context.spec.ts +++ b/libs/common/src/tools/integration/integration-context.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nService } from "../../platform/abstractions/i18n.service"; import { IntegrationContext } from "./integration-context"; import { IntegrationId } from "./integration-id"; diff --git a/libs/common/src/tools/integration/integration-context.ts b/libs/common/src/tools/integration/integration-context.ts index f30810ff81a..40648df6803 100644 --- a/libs/common/src/tools/integration/integration-context.ts +++ b/libs/common/src/tools/integration/integration-context.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { I18nService } from "../../platform/abstractions/i18n.service"; +import { Utils } from "../../platform/misc/utils"; import { IntegrationMetadata } from "./integration-metadata"; import { ApiSettings, IntegrationRequest } from "./rpc"; diff --git a/libs/common/src/tools/integration/rpc/rest-client.spec.ts b/libs/common/src/tools/integration/rpc/rest-client.spec.ts index e113ab9ff43..9b76d305e6f 100644 --- a/libs/common/src/tools/integration/rpc/rest-client.spec.ts +++ b/libs/common/src/tools/integration/rpc/rest-client.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ApiService } from "../../../abstractions/api.service"; +import { I18nService } from "../../../platform/abstractions/i18n.service"; import { IntegrationRequest } from "./integration-request"; import { RestClient } from "./rest-client"; diff --git a/libs/common/src/tools/integration/rpc/rest-client.ts b/libs/common/src/tools/integration/rpc/rest-client.ts index 287bb4f2573..c42244166e7 100644 --- a/libs/common/src/tools/integration/rpc/rest-client.ts +++ b/libs/common/src/tools/integration/rpc/rest-client.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ApiService } from "../../../abstractions/api.service"; +import { I18nService } from "../../../platform/abstractions/i18n.service"; import { IntegrationRequest } from "./integration-request"; import { JsonRpc } from "./rpc"; diff --git a/libs/common/src/tools/private-classifier.ts b/libs/common/src/tools/private-classifier.ts index de21f78f1c6..58244ae9906 100644 --- a/libs/common/src/tools/private-classifier.ts +++ b/libs/common/src/tools/private-classifier.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Classifier } from "@bitwarden/common/tools/state/classifier"; +import { Classifier } from "./state/classifier"; export class PrivateClassifier implements Classifier, Data> { constructor(private keys: (keyof Jsonify)[] = undefined) {} diff --git a/libs/common/src/tools/public-classifier.ts b/libs/common/src/tools/public-classifier.ts index e7c8c24ba78..e036ebd1c42 100644 --- a/libs/common/src/tools/public-classifier.ts +++ b/libs/common/src/tools/public-classifier.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Classifier } from "@bitwarden/common/tools/state/classifier"; +import { Classifier } from "./state/classifier"; export class PublicClassifier implements Classifier> { constructor(private keys: (keyof Jsonify)[]) {} diff --git a/libs/common/src/tools/rx.spec.ts b/libs/common/src/tools/rx.spec.ts index 9ce147a3ff4..2c433fef93b 100644 --- a/libs/common/src/tools/rx.spec.ts +++ b/libs/common/src/tools/rx.spec.ts @@ -56,7 +56,7 @@ describe("errorOnChange", () => { source$.complete(); - expect(complete).toBeTrue(); + expect(complete).toBe(true); }); it("errors when the input changes", async () => { diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index 74c0e77b394..fcc273d41bb 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -1,12 +1,12 @@ import { mock } from "jest-mock-extended"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; -import { KeyService } from "../../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc } from "../../../../../spec"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; +import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../../platform/services/container.service"; +import { UserKey } from "../../../../types/key"; import { SendType } from "../../enums/send-type"; import { SendData } from "../data/send.data"; @@ -123,7 +123,12 @@ describe("Send", () => { const view = await send.decrypt(); expect(text.decrypt).toHaveBeenNthCalledWith(1, "cryptoKey"); - expect(send.name.decrypt).toHaveBeenNthCalledWith(1, null, "cryptoKey"); + expect(send.name.decrypt).toHaveBeenNthCalledWith( + 1, + null, + "cryptoKey", + "Property: name; ObjectContext: No Domain Context", + ); expect(view).toMatchObject({ id: "id", diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index 43115b65937..c2390d439e7 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -81,6 +81,8 @@ export class Send extends Domain { const sendKeyEncryptionKey = await keyService.getUserKey(); model.key = await encryptService.decryptToBytes(this.key, sendKeyEncryptionKey); model.cryptoKey = await keyService.makeSendKey(model.key); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // TODO: error? } diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index a6427824a64..570f3e746a0 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -22,11 +22,6 @@ export abstract class SendApiService { postSend: (request: SendRequest) => Promise; postFileTypeSend: (request: SendRequest) => Promise; postSendFile: (sendId: string, fileId: string, data: FormData) => Promise; - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - postSendFileLegacy: (data: FormData) => Promise; putSend: (id: string, request: SendRequest) => Promise; putSendRemovePassword: (id: string) => Promise; deleteSend: (id: string) => Promise; diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index ff71408bce3..f709553646f 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -5,7 +5,6 @@ import { FileUploadApiMethods, FileUploadService, } from "../../../platform/abstractions/file-upload/file-upload.service"; -import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SendType } from "../enums/send-type"; import { SendData } from "../models/data/send.data"; @@ -106,15 +105,6 @@ export class SendApiService implements SendApiServiceAbstraction { return this.apiService.send("POST", "/sends/" + sendId + "/file/" + fileId, data, true, false); } - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postSendFileLegacy(data: FormData): Promise { - const r = await this.apiService.send("POST", "/sends/file", data, true, true); - return new SendResponse(r); - } - async putSend(id: string, request: SendRequest): Promise { const r = await this.apiService.send("PUT", "/sends/" + id, request, true, true); return new SendResponse(r); @@ -173,9 +163,7 @@ export class SendApiService implements SendApiServiceAbstraction { this.generateMethods(uploadDataResponse, response), ); } catch (e) { - if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { - response = await this.legacyServerSendFileUpload(sendData, request); - } else if (e instanceof ErrorResponse) { + if (e instanceof ErrorResponse) { throw new Error((e as ErrorResponse).getSingleMessage()); } else { throw e; @@ -219,35 +207,4 @@ export class SendApiService implements SendApiServiceAbstraction { return this.deleteSend(sendId); }; } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async legacyServerSendFileUpload( - sendData: [Send, EncArrayBuffer], - request: SendRequest, - ): Promise { - const fd = new FormData(); - try { - const blob = new Blob([sendData[1].buffer], { type: "application/octet-stream" }); - fd.append("model", JSON.stringify(request)); - fd.append("data", blob, sendData[0].file.fileName.encryptedString); - } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append("model", JSON.stringify(request)); - fd.append( - "data", - Buffer.from(sendData[1].buffer) as any, - { - filepath: sendData[0].file.fileName.encryptedString, - contentType: "application/octet-stream", - } as any, - ); - } else { - throw e; - } - } - return await this.postSendFileLegacy(fd); - } } diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 5aca3a4b5c9..662fee02bbf 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -1,10 +1,8 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { KeyService } from "@bitwarden/key-management"; -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeActiveUserState, @@ -13,12 +11,14 @@ import { mockAccountServiceWith, } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EnvironmentService } from "../../../platform/abstractions/environment.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; +import { SelfHostedEnvironment } from "../../../platform/services/default-environment.service"; import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { SendType } from "../enums/send-type"; diff --git a/libs/common/src/tools/state/user-state-subject.spec.ts b/libs/common/src/tools/state/user-state-subject.spec.ts index 6a50a1dd668..8111f6f9f17 100644 --- a/libs/common/src/tools/state/user-state-subject.spec.ts +++ b/libs/common/src/tools/state/user-state-subject.spec.ts @@ -1,9 +1,8 @@ import { BehaviorSubject, of, Subject } from "rxjs"; -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - import { awaitAsync, FakeSingleUserState, ObservableTracker } from "../../../spec"; +import { GENERATOR_DISK, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; import { UserBound } from "../dependencies"; import { PrivateClassifier } from "../private-classifier"; diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index d508cf71663..c00560304e4 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -26,10 +26,9 @@ import { skip, } from "rxjs"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SingleUserState, UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - +import { EncString } from "../../platform/models/domain/enc-string"; +import { SingleUserState, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; import { UserBound } from "../dependencies"; import { anyComplete, errorOnChange, ready, withLatestReady } from "../rx"; diff --git a/libs/common/src/tools/util.ts b/libs/common/src/tools/util.ts new file mode 100644 index 00000000000..9a3a14c1c83 --- /dev/null +++ b/libs/common/src/tools/util.ts @@ -0,0 +1,19 @@ +/** Recursively freeze an object's own keys + * @param value the value to freeze + * @returns `value` + * @remarks this function is derived from MDN's `deepFreeze`, which + * has been committed to the public domain. + */ +export function deepFreeze(value: T): Readonly { + const keys = Reflect.ownKeys(value) as (keyof T)[]; + + for (const key of keys) { + const own = value[key]; + + if ((own && typeof own === "object") || typeof own === "function") { + deepFreeze(own); + } + } + + return Object.freeze(value); +} diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 59dbe28f907..5ad498c115a 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -10,3 +10,4 @@ export type PolicyId = Opaque; export type CipherId = Opaque; export type SendId = Opaque; export type IndexedEntityId = Opaque; +export type SecurityTaskId = Opaque; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 870cb8b3d73..88cd476606d 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { Observable } from "rxjs"; -import { LocalData } from "@bitwarden/common/vault/models/data/local.data"; import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; @@ -11,6 +10,7 @@ import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid import { UserKey } from "../../types/key"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; +import { LocalData } from "../models/data/local.data"; import { Cipher } from "../models/domain/cipher"; import { Field } from "../models/domain/field"; import { CipherWithIdRequest } from "../models/request/cipher-with-id.request"; @@ -26,6 +26,12 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + /** + * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. + * + * An empty array indicates that all ciphers were successfully decrypted. + */ + failedToDecryptCiphers$: Observable; clearCache: (userId?: string) => Promise; encrypt: ( model: CipherView, diff --git a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts index a732acfa991..1bb4a52e929 100644 --- a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts @@ -1,11 +1,14 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore + +import { UserId } from "../../../types/guid"; +import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; import { FolderResponse } from "../../models/response/folder.response"; export class FolderApiServiceAbstraction { - save: (folder: Folder) => Promise; - delete: (id: string) => Promise; + save: (folder: Folder, userId: UserId) => Promise; + delete: (id: string, userId: UserId) => Promise; get: (id: string) => Promise; - deleteAll: () => Promise; + deleteAll: (userId: UserId) => Promise; } diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index c5722e0f57b..b7241e3ae37 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -13,23 +13,27 @@ import { FolderWithIdRequest } from "../../models/request/folder-with-id.request import { FolderView } from "../../models/view/folder.view"; export abstract class FolderService implements UserKeyRotationDataProvider { - folders$: Observable; - folderViews$: Observable; + folders$: (userId: UserId) => Observable; + folderViews$: (userId: UserId) => Observable; - clearCache: () => Promise; + clearDecryptedFolderState: (userId: UserId) => Promise; encrypt: (model: FolderView, key: SymmetricCryptoKey) => Promise; - get: (id: string) => Promise; - getDecrypted$: (id: string) => Observable; - getAllFromState: () => Promise; + get: (id: string, userId: UserId) => Promise; + getDecrypted$: (id: string, userId: UserId) => Observable; + /** + * @deprecated Use firstValueFrom(folders$) directly instead + * @param userId The user id + * @returns Promise of folders array + */ + getAllFromState: (userId: UserId) => Promise; /** * @deprecated Only use in CLI! */ - getFromState: (id: string) => Promise; + getFromState: (id: string, userId: UserId) => Promise; /** * @deprecated Only use in CLI! */ - getAllDecryptedFromState: () => Promise; - decryptFolders: (folders: Folder[]) => Promise; + getAllDecryptedFromState: (userId: UserId) => Promise; /** * Returns user folders re-encrypted with the new user key. * @param originalUserKey the original user key @@ -46,8 +50,8 @@ export abstract class FolderService implements UserKeyRotationDataProvider Promise; + upsert: (folder: FolderData | FolderData[], userId: UserId) => Promise; replace: (folders: { [id: string]: FolderData }, userId: UserId) => Promise; - clear: (userId?: string) => Promise; - delete: (id: string | string[]) => Promise; + clear: (userId: UserId) => Promise; + delete: (id: string | string[], userId: UserId) => Promise; } diff --git a/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts b/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts index 9b3fde47a15..ea1e73c2685 100644 --- a/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts +++ b/libs/common/src/vault/abstractions/vault-settings/vault-settings.service.ts @@ -19,6 +19,12 @@ export abstract class VaultSettingsService { */ showIdentitiesCurrentTab$: Observable; /** + /** + * An observable monitoring the state of the click items on the Vault view + * for Autofill suggestions. + */ + clickItemsToAutofillVaultView$: Observable; + /** /** * Saves the enable passkeys setting to disk. @@ -35,4 +41,10 @@ export abstract class VaultSettingsService { * @param value The new value for the show identities on tab page setting. */ setShowIdentitiesCurrentTab: (value: boolean) => Promise; + /** + * Saves the click items on vault View for Autofill suggestions to disk. + * @param value The new value for the click items on vault View for + * Autofill suggestions setting. + */ + setClickItemsToAutofillVaultView: (value: boolean) => Promise; } diff --git a/libs/common/src/vault/icon/build-cipher-icon.ts b/libs/common/src/vault/icon/build-cipher-icon.ts index 78e6ecd7b4f..5775bc7f55e 100644 --- a/libs/common/src/vault/icon/build-cipher-icon.ts +++ b/libs/common/src/vault/icon/build-cipher-icon.ts @@ -43,10 +43,18 @@ export function buildCipherIcon(iconsServerUrl: string, cipher: CipherView, show isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1; } + if (isWebsite && (hostnameUri.endsWith(".onion") || hostnameUri.endsWith(".i2p"))) { + image = null; + fallbackImage = "images/bwi-globe.png"; + break; + } + if (showFavicon && isWebsite) { try { image = `${iconsServerUrl}/${Utils.getHostname(hostnameUri)}/icon.png`; fallbackImage = "images/bwi-globe.png"; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Ignore error since the fallback icon will be shown if image is null. } diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 14dec8dea0c..8cae7170738 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; @@ -101,7 +103,7 @@ describe("Attachment", () => { it("uses the provided key without depending on KeyService", async () => { const providedKey = mock(); - await attachment.decrypt(null, providedKey); + await attachment.decrypt(null, "", providedKey); expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey); @@ -111,7 +113,7 @@ describe("Attachment", () => { const orgKey = mock(); keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey); - await attachment.decrypt("orgId", null); + await attachment.decrypt("orgId", "", null); expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId"); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey); @@ -121,7 +123,7 @@ describe("Attachment", () => { const userKey = mock(); keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey); - await attachment.decrypt(null, null); + await attachment.decrypt(null, "", null); expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 1178f441c5e..4eee0307746 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -38,7 +38,11 @@ export class Attachment extends Domain { ); } - async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { const view = await this.decryptObj( new AttachmentView(this), { @@ -46,6 +50,7 @@ export class Attachment extends Domain { }, orgId, encKey, + "DomainType: Attachment; " + context, ); if (this.key != null) { @@ -64,6 +69,8 @@ export class Attachment extends Domain { const encryptService = Utils.getContainerService().getEncryptService(); const decValue = await encryptService.decryptToBytes(this.key, encKey); return new SymmetricCryptoKey(decValue); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // TODO: error? } diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 739cbf78465..fccfe3f595b 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -37,7 +37,11 @@ export class Card extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new CardView(), { @@ -50,6 +54,7 @@ export class Card extends Domain { }, orgId, encKey, + "DomainType: Card; " + context, ); } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 509a17a8a0e..dd79da3086e 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; @@ -10,6 +10,7 @@ import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; +import { UserId } from "../../../types/guid"; import { CipherService } from "../../abstractions/cipher.service"; import { FieldType, SecureNoteType } from "../../enums"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index d7355ce5202..d82f4585e65 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -136,7 +136,17 @@ export class Cipher extends Domain implements Decryptable { if (this.key != null) { const encryptService = Utils.getContainerService().getEncryptService(); - encKey = new SymmetricCryptoKey(await encryptService.decryptToBytes(this.key, encKey)); + const keyBytes = await encryptService.decryptToBytes( + this.key, + encKey, + `Cipher Id: ${this.id}; Content: CipherKey; IsEncryptedByOrgKey: ${this.organizationId != null}`, + ); + if (keyBytes == null) { + model.name = "[error: cannot decrypt]"; + model.decryptionFailure = true; + return model; + } + encKey = new SymmetricCryptoKey(keyBytes); bypassValidation = false; } @@ -152,19 +162,36 @@ export class Cipher extends Domain implements Decryptable { switch (this.type) { case CipherType.Login: - model.login = await this.login.decrypt(this.organizationId, bypassValidation, encKey); + model.login = await this.login.decrypt( + this.organizationId, + bypassValidation, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.SecureNote: - model.secureNote = await this.secureNote.decrypt(this.organizationId, encKey); + model.secureNote = await this.secureNote.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.Card: - model.card = await this.card.decrypt(this.organizationId, encKey); + model.card = await this.card.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); break; case CipherType.Identity: - model.identity = await this.identity.decrypt(this.organizationId, encKey); + model.identity = await this.identity.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.SshKey: - model.sshKey = await this.sshKey.decrypt(this.organizationId, encKey); + model.sshKey = await this.sshKey.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; default: break; @@ -175,7 +202,7 @@ export class Cipher extends Domain implements Decryptable { await this.attachments.reduce((promise, attachment) => { return promise .then(() => { - return attachment.decrypt(this.organizationId, encKey); + return attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); }) .then((decAttachment) => { attachments.push(decAttachment); diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index e2b7aef52f0..570e6c0b4d5 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -61,7 +61,11 @@ export class Identity extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context: string = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new IdentityView(), { @@ -86,6 +90,7 @@ export class Identity extends Domain { }, orgId, encKey, + "DomainType: Identity; " + context, ); } diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 0d7380e034d..36782a81502 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -33,7 +33,11 @@ export class LoginUri extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context: string = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new LoginUriView(this), { @@ -41,6 +45,7 @@ export class LoginUri extends Domain { }, orgId, encKey, + context, ); } diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index a0a61a9b857..f9a85cd818e 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -55,6 +55,7 @@ export class Login extends Domain { async decrypt( orgId: string, bypassValidation: boolean, + context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { const view = await this.decryptObj( @@ -66,6 +67,7 @@ export class Login extends Domain { }, orgId, encKey, + `DomainType: Login; ${context}`, ); if (this.uris != null) { @@ -76,7 +78,7 @@ export class Login extends Domain { continue; } - const uri = await this.uris[i].decrypt(orgId, encKey); + const uri = await this.uris[i].decrypt(orgId, context, encKey); // URIs are shared remotely after decryption // we need to validate that the string hasn't been changed by a compromised server // This validation is tied to the existence of cypher.key for backwards compatibility diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index 4c4f465654e..48063f495f0 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -32,6 +32,7 @@ export class Password extends Domain { }, orgId, encKey, + "DomainType: PasswordHistory", ); } diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index 4769ad062d9..693ae38d9fb 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -20,8 +20,12 @@ export class SecureNote extends Domain { this.type = obj.type; } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { - return Promise.resolve(new SecureNoteView(this)); + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { + return new SecureNoteView(this); } toSecureNoteData(): SecureNoteData { diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index 3a79c1f0022..b4df172e543 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; - import Domain from "../../../platform/models/domain/domain-base"; +import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SshKeyData } from "../data/ssh-key.data"; import { SshKeyView } from "../view/ssh-key.view"; @@ -32,7 +31,11 @@ export class SshKey extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new SshKeyView(), { @@ -42,6 +45,7 @@ export class SshKey extends Domain { }, orgId, encKey, + "DomainType: SshKey; " + context, ); } diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 6416ab31970..20dbd23065c 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -46,6 +46,11 @@ export class CipherView implements View, InitializerMetadata { deletedDate: Date = null; reprompt: CipherRepromptType = CipherRepromptType.None; + /** + * Flag to indicate if the cipher decryption failed. + */ + decryptionFailure = false; + constructor(c?: Cipher) { if (!c) { return; @@ -150,6 +155,8 @@ export class CipherView implements View, InitializerMetadata { return null; } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars const item = this.item; return this.item[linkedFieldOption.propertyKey as keyof typeof item]; } diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index a6b7a0c0a22..315adb87c75 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -142,6 +142,8 @@ export class LoginUriView implements View { try { const regex = new RegExp(this.uri, "i"); return regex.test(targetUri); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Invalid regex return false; diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index cccd29ad697..15e8b03bfad 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -2,10 +2,10 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { CollectionId } from "@bitwarden/common/types/guid"; +import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "../../admin-console/models/domain/organization"; +import { CollectionId } from "../../types/guid"; import { CipherView } from "../models/view/cipher.view"; import { diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 260d1eeece7..025d6b1cdc3 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -3,9 +3,9 @@ import { map, Observable, of, shareReplay, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { CollectionId } from "@bitwarden/common/types/guid"; +import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionId } from "../../types/guid"; import { Cipher } from "../models/domain/cipher"; import { CipherView } from "../models/view/cipher.view"; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 6b225af0d84..0d6578f165d 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, map, of } from "rxjs"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherDecryptionKeys, KeyService, @@ -15,6 +15,7 @@ import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; +import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -359,6 +360,7 @@ describe("Cipher Service", () => { const originalUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; let decryptedCiphers: BehaviorSubject>; + let failedCiphers: BehaviorSubject; let encryptedKey: EncString; beforeEach(() => { @@ -385,6 +387,7 @@ describe("Cipher Service", () => { Cipher2: cipher2, }); cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))); + cipherService.failedToDecryptCiphers$ = failedCiphers = new BehaviorSubject([]); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Cipher Key"); @@ -413,5 +416,16 @@ describe("Cipher Service", () => { "New user key is required to rotate ciphers", ); }); + + it("throws if the user has any failed to decrypt ciphers", async () => { + const badCipher = new CipherView(cipherObj); + badCipher.id = "Cipher 3"; + badCipher.organizationId = null; + badCipher.decryptionFailure = true; + failedCiphers.next([badCipher]); + await expect( + cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId), + ).rejects.toThrow("Cannot rotate ciphers when decryption failures are present"); + }); }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8711496b374..fe946fbb064 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -7,25 +7,27 @@ import { map, merge, Observable, + of, shareReplay, Subject, switchMap, } from "rxjs"; import { SemVer } from "semver"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; +import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -79,6 +81,7 @@ import { ADD_EDIT_CIPHER_INFO_KEY, DECRYPTED_CIPHERS, ENCRYPTED_CIPHERS, + FAILED_DECRYPTED_CIPHERS, LOCAL_DATA_KEY, } from "./key-state/ciphers.state"; @@ -109,9 +112,17 @@ export class CipherService implements CipherServiceAbstraction { cipherViews$: Observable; addEditCipherInfo$: Observable; + /** + * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. + * + * An empty array indicates that all ciphers were successfully decrypted. + */ + failedToDecryptCiphers$: Observable; + private localDataState: ActiveUserState>; private encryptedCiphersState: ActiveUserState>; private decryptedCiphersState: ActiveUserState>; + private failedToDecryptCiphersState: ActiveUserState; private addEditCipherInfoState: ActiveUserState; constructor( @@ -132,6 +143,7 @@ export class CipherService implements CipherServiceAbstraction { this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); + this.failedToDecryptCiphersState = this.stateProvider.getActive(FAILED_DECRYPTED_CIPHERS); this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); @@ -143,6 +155,13 @@ export class CipherService implements CipherServiceAbstraction { switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())), shareReplay({ bufferSize: 1, refCount: true }), ); + + this.failedToDecryptCiphers$ = this.failedToDecryptCiphersState.state$.pipe( + filter((ciphers) => ciphers != null), + switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))), + shareReplay({ bufferSize: 1, refCount: true }), + ); + this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } @@ -162,6 +181,10 @@ export class CipherService implements CipherServiceAbstraction { } } + async setFailedDecryptedCiphers(cipherViews: CipherView[], userId: UserId) { + await this.stateProvider.setUserState(FAILED_DECRYPTED_CIPHERS, cipherViews, userId); + } + private async setDecryptedCiphers(value: CipherView[], userId: UserId) { const cipherViews: { [id: string]: CipherView } = {}; value?.forEach((c) => { @@ -378,7 +401,7 @@ export class CipherService implements CipherServiceAbstraction { */ @sequentialize(() => "getAllDecrypted") async getAllDecrypted(): Promise { - let decCiphers = await this.getDecryptedCiphers(); + const decCiphers = await this.getDecryptedCiphers(); if (decCiphers != null && decCiphers.length !== 0) { await this.reindexCiphers(); return await this.getDecryptedCiphers(); @@ -390,10 +413,15 @@ export class CipherService implements CipherServiceAbstraction { return []; } - decCiphers = await this.decryptCiphers(await this.getAll(), activeUserId); + const [newDecCiphers, failedCiphers] = await this.decryptCiphers( + await this.getAll(), + activeUserId, + ); - await this.setDecryptedCipherCache(decCiphers, activeUserId); - return decCiphers; + await this.setDecryptedCipherCache(newDecCiphers, activeUserId); + await this.setFailedDecryptedCiphers(failedCiphers, activeUserId); + + return newDecCiphers; } private async getDecryptedCiphers() { @@ -402,7 +430,17 @@ export class CipherService implements CipherServiceAbstraction { ); } - private async decryptCiphers(ciphers: Cipher[], userId: UserId) { + /** + * Decrypts the provided ciphers using the provided user's keys. + * @param ciphers + * @param userId + * @returns Two cipher arrays, the first containing successfully decrypted ciphers and the second containing ciphers that failed to decrypt. + * @private + */ + private async decryptCiphers( + ciphers: Cipher[], + userId: UserId, + ): Promise<[CipherView[], CipherView[]]> { const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { @@ -420,7 +458,7 @@ export class CipherService implements CipherServiceAbstraction { {} as Record, ); - const decCiphers = ( + const allCipherViews = ( await Promise.all( Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { @@ -440,7 +478,18 @@ export class CipherService implements CipherServiceAbstraction { .flat() .sort(this.getLocaleSortingFunction()); - return decCiphers; + // Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt + return allCipherViews.reduce( + (acc, c) => { + if (c.decryptionFailure) { + acc[1].push(c); + } else { + acc[0].push(c); + } + return acc; + }, + [[], []] as [CipherView[], CipherView[]], + ); } private async reindexCiphers() { @@ -1272,10 +1321,15 @@ export class CipherService implements CipherServiceAbstraction { let encryptedCiphers: CipherWithIdRequest[] = []; const ciphers = await firstValueFrom(this.cipherViews$); + const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$); if (!ciphers) { return encryptedCiphers; } + if (failedCiphers.length > 0) { + throw new Error("Cannot rotate ciphers when decryption failures are present"); + } + const userCiphers = ciphers.filter((c) => c.organizationId == null); if (userCiphers.length === 0) { return encryptedCiphers; @@ -1636,6 +1690,7 @@ export class CipherService implements CipherServiceAbstraction { private async clearDecryptedCiphersState(userId: UserId) { await this.setDecryptedCiphers(null, userId); + await this.setFailedDecryptedCiphers(null, userId); this.clearSortedCiphers(); } diff --git a/libs/common/src/vault/services/folder/folder-api.service.ts b/libs/common/src/vault/services/folder/folder-api.service.ts index e46df37c176..fe9c3218a84 100644 --- a/libs/common/src/vault/services/folder/folder-api.service.ts +++ b/libs/common/src/vault/services/folder/folder-api.service.ts @@ -1,4 +1,5 @@ import { ApiService } from "../../../abstractions/api.service"; +import { UserId } from "../../../types/guid"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; import { FolderData } from "../../../vault/models/data/folder.data"; @@ -12,7 +13,7 @@ export class FolderApiService implements FolderApiServiceAbstraction { private apiService: ApiService, ) {} - async save(folder: Folder): Promise { + async save(folder: Folder, userId: UserId): Promise { const request = new FolderRequest(folder); let response: FolderResponse; @@ -24,17 +25,18 @@ export class FolderApiService implements FolderApiServiceAbstraction { } const data = new FolderData(response); - await this.folderService.upsert(data); + await this.folderService.upsert(data, userId); + return data; } - async delete(id: string): Promise { + async delete(id: string, userId: UserId): Promise { await this.deleteFolder(id); - await this.folderService.delete(id); + await this.folderService.delete(id, userId); } - async deleteAll(): Promise { + async deleteAll(userId: UserId): Promise { await this.apiService.send("DELETE", "/folders/all", null, true, false); - await this.folderService.clear(); + await this.folderService.clear(userId); } async get(id: string): Promise { diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 193d0e85e61..cc3aa1946ca 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,10 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; -import { makeStaticByteArray } from "../../../../spec"; +import { makeEncString } from "../../../../spec"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; -import { FakeActiveUserState } from "../../../../spec/fake-state"; +import { FakeSingleUserState } from "../../../../spec/fake-state"; import { FakeStateProvider } from "../../../../spec/fake-state-provider"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; @@ -17,7 +19,7 @@ import { CipherService } from "../../abstractions/cipher.service"; import { FolderData } from "../../models/data/folder.data"; import { FolderView } from "../../models/view/folder.view"; import { FolderService } from "../../services/folder/folder.service"; -import { FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state"; +import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state"; describe("Folder Service", () => { let folderService: FolderService; @@ -30,7 +32,7 @@ describe("Folder Service", () => { const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; - let folderState: FakeActiveUserState>; + let folderState: FakeSingleUserState>; beforeEach(() => { keyService = mock(); @@ -42,11 +44,9 @@ describe("Folder Service", () => { stateProvider = new FakeStateProvider(accountService); i18nService.collator = new Intl.Collator("en"); + i18nService.t.mockReturnValue("No Folder"); - keyService.hasUserKey.mockResolvedValue(true); - keyService.getUserKeyWithLegacySupport.mockResolvedValue( - new SymmetricCryptoKey(makeStaticByteArray(32)) as UserKey, - ); + keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); encryptService.decryptToUtf8.mockResolvedValue("DEC"); folderService = new FolderService( @@ -57,10 +57,53 @@ describe("Folder Service", () => { stateProvider, ); - folderState = stateProvider.activeUser.getFake(FOLDER_ENCRYPTED_FOLDERS); + folderState = stateProvider.singleUser.getFake(mockUserId, FOLDER_ENCRYPTED_FOLDERS); // Initial state - folderState.nextState({ "1": folderData("1", "test") }); + folderState.nextState({ "1": folderData("1") }); + }); + + describe("folders$", () => { + it("emits encrypted folders from state", async () => { + const folder1 = folderData("1"); + const folder2 = folderData("2"); + + await stateProvider.setUserState( + FOLDER_ENCRYPTED_FOLDERS, + Object.fromEntries([folder1, folder2].map((f) => [f.id, f])), + mockUserId, + ); + + const result = await firstValueFrom(folderService.folders$(mockUserId)); + + expect(result.length).toBe(2); + expect(result).toContainPartialObjects([ + { id: "1", name: makeEncString("ENC_STRING_1") }, + { id: "2", name: makeEncString("ENC_STRING_2") }, + ]); + }); + }); + + describe("folderView$", () => { + it("emits decrypted folders from state", async () => { + const folder1 = folderData("1"); + const folder2 = folderData("2"); + + await stateProvider.setUserState( + FOLDER_ENCRYPTED_FOLDERS, + Object.fromEntries([folder1, folder2].map((f) => [f.id, f])), + mockUserId, + ); + + const result = await firstValueFrom(folderService.folderViews$(mockUserId)); + + expect(result.length).toBe(3); + expect(result).toContainPartialObjects([ + { id: "1", name: "DEC" }, + { id: "2", name: "DEC" }, + { name: "No Folder" }, + ]); + }); }); it("encrypt", async () => { @@ -83,105 +126,83 @@ describe("Folder Service", () => { describe("get", () => { it("exists", async () => { - const result = await folderService.get("1"); + const result = await folderService.get("1", mockUserId); expect(result).toEqual({ id: "1", - name: { - encryptedString: "test", - encryptionType: 0, - }, + name: makeEncString("ENC_STRING_" + 1), revisionDate: null, }); }); it("not exists", async () => { - const result = await folderService.get("2"); + const result = await folderService.get("2", mockUserId); expect(result).toBe(undefined); }); }); it("upsert", async () => { - await folderService.upsert(folderData("2", "test 2")); + await folderService.upsert(folderData("2"), mockUserId); - expect(await firstValueFrom(folderService.folders$)).toEqual([ + expect(await firstValueFrom(folderService.folders$(mockUserId))).toEqual([ { id: "1", - name: { - encryptedString: "test", - encryptionType: 0, - }, + name: makeEncString("ENC_STRING_" + 1), revisionDate: null, }, { id: "2", - name: { - encryptedString: "test 2", - encryptionType: 0, - }, + name: makeEncString("ENC_STRING_" + 2), revisionDate: null, }, ]); }); it("replace", async () => { - await folderService.replace({ "2": folderData("2", "test 2") }, mockUserId); + await folderService.replace({ "4": folderData("4") }, mockUserId); - expect(await firstValueFrom(folderService.folders$)).toEqual([ + expect(await firstValueFrom(folderService.folders$(mockUserId))).toEqual([ { - id: "2", - name: { - encryptedString: "test 2", - encryptionType: 0, - }, + id: "4", + name: makeEncString("ENC_STRING_" + 4), revisionDate: null, }, ]); }); it("delete", async () => { - await folderService.delete("1"); + await folderService.delete("1", mockUserId); - expect((await firstValueFrom(folderService.folders$)).length).toBe(0); + expect((await firstValueFrom(folderService.folders$(mockUserId))).length).toBe(0); }); - it("clearCache", async () => { - await folderService.clearCache(); - - expect((await firstValueFrom(folderService.folders$)).length).toBe(1); - expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0); - }); - - describe("clear", () => { + describe("clearDecryptedFolderState", () => { it("null userId", async () => { - await folderService.clear(); - - expect((await firstValueFrom(folderService.folders$)).length).toBe(0); - expect((await firstValueFrom(folderService.folderViews$)).length).toBe(0); + await expect(folderService.clearDecryptedFolderState(null)).rejects.toThrow( + "User ID is required.", + ); }); - /** - * TODO: Fix this test to address the problem where the fakes for the active user state is not - * updated as expected - */ - // it("matching userId", async () => { - // stateService.getUserId.mockResolvedValue("1"); - // await folderService.clear("1" as UserId); + it("userId provided", async () => { + await folderService.clearDecryptedFolderState(mockUserId); - // expect((await firstValueFrom(folderService.folders$)).length).toBe(0); - // }); + expect((await firstValueFrom(folderService.folders$(mockUserId))).length).toBe(1); + expect( + (await firstValueFrom(stateProvider.getUserState$(FOLDER_DECRYPTED_FOLDERS, mockUserId))) + .length, + ).toBe(0); + }); + }); - /** - * TODO: Fix this test to address the problem where the fakes for the active user state is not - * updated as expected - */ - // it("mismatching userId", async () => { - // await folderService.clear("12" as UserId); + it("clear", async () => { + await folderService.clear(mockUserId); - // expect((await firstValueFrom(folderService.folders$)).length).toBe(1); - // expect((await firstValueFrom(folderService.folderViews$)).length).toBe(2); - // }); + expect((await firstValueFrom(folderService.folders$(mockUserId))).length).toBe(0); + + const folderViews = await firstValueFrom(folderService.folderViews$(mockUserId)); + expect(folderViews.length).toBe(1); + expect(folderViews[0].id).toBeNull(); // Should be the "No Folder" folder }); describe("getRotatedData", () => { @@ -207,10 +228,10 @@ describe("Folder Service", () => { }); }); - function folderData(id: string, name: string) { + function folderData(id: string) { const data = new FolderData({} as any); data.id = id; - data.name = name; + data.name = makeEncString("ENC_STRING_" + data.id).encryptedString; return data; } diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 07b162b4f14..c21a92fd894 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -1,14 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { Observable, firstValueFrom, map, shareReplay } from "rxjs"; - -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } from "rxjs"; +import { EncryptService } from ".././../../platform/abstractions/encrypt.service"; +import { Utils } from ".././../../platform/misc/utils"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; -import { ActiveUserState, DerivedState, StateProvider } from "../../../platform/state"; +import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { CipherService } from "../../../vault/abstractions/cipher.service"; @@ -21,11 +22,18 @@ import { FolderWithIdRequest } from "../../models/request/folder-with-id.request import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state"; export class FolderService implements InternalFolderServiceAbstraction { - folders$: Observable; - folderViews$: Observable; + /** + * Ensures we reuse the same observable stream for each userId rather than + * creating a new one on each folderViews$ call. + */ + private folderViewCache = new Map>(); - private encryptedFoldersState: ActiveUserState>; - private decryptedFoldersState: DerivedState; + /** + * Used to force the folderviews$ Observable to re-emit with a provided value. + * Required because shareReplay with refCount: false maintains last emission. + * Used during cleanup to force emit empty arrays, ensuring stale data isn't retained. + */ + private forceFolderViews: Record> = {}; constructor( private keyService: KeyService, @@ -33,23 +41,44 @@ export class FolderService implements InternalFolderServiceAbstraction { private i18nService: I18nService, private cipherService: CipherService, private stateProvider: StateProvider, - ) { - this.encryptedFoldersState = this.stateProvider.getActive(FOLDER_ENCRYPTED_FOLDERS); - this.decryptedFoldersState = this.stateProvider.getDerived( - this.encryptedFoldersState.state$, - FOLDER_DECRYPTED_FOLDERS, - { folderService: this, keyService: this.keyService }, - ); + ) {} - this.folders$ = this.encryptedFoldersState.state$.pipe( - map((folderData) => Object.values(folderData).map((f) => new Folder(f))), - ); + folders$(userId: UserId): Observable { + return this.encryptedFoldersState(userId).state$.pipe( + map((folders) => { + if (folders == null) { + return []; + } - this.folderViews$ = this.decryptedFoldersState.state$; + return Object.values(folders).map((f) => new Folder(f)); + }), + ); } - async clearCache(): Promise { - await this.decryptedFoldersState.forceValue([]); + /** + * Returns an Observable of decrypted folder views for the given userId. + * Uses folderViewCache to maintain a single Observable instance per user, + * combining normal folder state updates with forced updates. + */ + folderViews$(userId: UserId): Observable { + if (!this.folderViewCache.has(userId)) { + if (!this.forceFolderViews[userId]) { + this.forceFolderViews[userId] = new Subject(); + } + + const observable = merge( + this.forceFolderViews[userId], + this.encryptedFoldersState(userId).state$.pipe( + switchMap((folderData) => { + return this.decryptFolders(userId, folderData); + }), + ), + ).pipe(shareReplay({ refCount: false, bufferSize: 1 })); + + this.folderViewCache.set(userId, observable); + } + + return this.folderViewCache.get(userId); } // TODO: This should be moved to EncryptService or something @@ -60,29 +89,29 @@ export class FolderService implements InternalFolderServiceAbstraction { return folder; } - async get(id: string): Promise { - const folders = await firstValueFrom(this.folders$); + async get(id: string, userId: UserId): Promise { + const folders = await firstValueFrom(this.folders$(userId)); return folders.find((folder) => folder.id === id); } - getDecrypted$(id: string): Observable { - return this.folderViews$.pipe( + getDecrypted$(id: string, userId: UserId): Observable { + return this.folderViews$(userId).pipe( map((folders) => folders.find((folder) => folder.id === id)), shareReplay({ refCount: true, bufferSize: 1 }), ); } - async getAllFromState(): Promise { - return await firstValueFrom(this.folders$); + async getAllFromState(userId: UserId): Promise { + return await firstValueFrom(this.folders$(userId)); } /** * @deprecated For the CLI only * @param id id of the folder */ - async getFromState(id: string): Promise { - const folder = await this.get(id); + async getFromState(id: string, userId: UserId): Promise { + const folder = await this.get(id, userId); if (!folder) { return null; } @@ -93,12 +122,13 @@ export class FolderService implements InternalFolderServiceAbstraction { /** * @deprecated Only use in CLI! */ - async getAllDecryptedFromState(): Promise { - return await firstValueFrom(this.folderViews$); + async getAllDecryptedFromState(userId: UserId): Promise { + return await firstValueFrom(this.folderViews$(userId)); } - async upsert(folderData: FolderData | FolderData[]): Promise { - await this.encryptedFoldersState.update((folders) => { + async upsert(folderData: FolderData | FolderData[], userId: UserId): Promise { + await this.clearDecryptedFolderState(userId); + await this.encryptedFoldersState(userId).update((folders) => { if (folders == null) { folders = {}; } @@ -120,24 +150,31 @@ export class FolderService implements InternalFolderServiceAbstraction { if (!folders) { return; } - + await this.clearDecryptedFolderState(userId); await this.stateProvider.getUser(userId, FOLDER_ENCRYPTED_FOLDERS).update(() => { const newFolders: Record = { ...folders }; return newFolders; }); } - async clear(userId?: UserId): Promise { + async clearDecryptedFolderState(userId: UserId): Promise { if (userId == null) { - await this.encryptedFoldersState.update(() => ({})); - await this.decryptedFoldersState.forceValue([]); - } else { - await this.stateProvider.getUser(userId, FOLDER_ENCRYPTED_FOLDERS).update(() => ({})); + throw new Error("User ID is required."); } + + await this.setDecryptedFolders([], userId); } - async delete(id: string | string[]): Promise { - await this.encryptedFoldersState.update((folders) => { + async clear(userId: UserId): Promise { + this.forceFolderViews[userId]?.next([]); + + await this.encryptedFoldersState(userId).update(() => ({})); + await this.clearDecryptedFolderState(userId); + } + + async delete(id: string | string[], userId: UserId): Promise { + await this.clearDecryptedFolderState(userId); + await this.encryptedFoldersState(userId).update((folders) => { if (folders == null) { return; } @@ -164,25 +201,11 @@ export class FolderService implements InternalFolderServiceAbstraction { } } if (updates.length > 0) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.cipherService.upsert(updates.map((c) => c.toCipherData())); + await this.cipherService.upsert(updates.map((c) => c.toCipherData())); } } } - async decryptFolders(folders: Folder[]) { - const decryptFolderPromises = folders.map((f) => f.decrypt()); - const decryptedFolders = await Promise.all(decryptFolderPromises); - - decryptedFolders.sort(Utils.getSortFunction(this.i18nService, "name")); - - const noneFolder = new FolderView(); - noneFolder.name = this.i18nService.t("noneFolder"); - decryptedFolders.push(noneFolder); - return decryptedFolders; - } - async getRotatedData( originalUserKey: UserKey, newUserKey: UserKey, @@ -193,7 +216,7 @@ export class FolderService implements InternalFolderServiceAbstraction { } let encryptedFolders: FolderWithIdRequest[] = []; - const folders = await firstValueFrom(this.folderViews$); + const folders = await firstValueFrom(this.folderViews$(userId)); if (!folders) { return encryptedFolders; } @@ -205,4 +228,63 @@ export class FolderService implements InternalFolderServiceAbstraction { ); return encryptedFolders; } + + /** + * Decrypts the folders for a user. + * @param userId the user id + * @param folderData encrypted folders + * @returns a list of decrypted folders + */ + private async decryptFolders( + userId: UserId, + folderData: Record, + ): Promise { + // Check if the decrypted folders are already cached + const decrypted = await firstValueFrom( + this.stateProvider.getUser(userId, FOLDER_DECRYPTED_FOLDERS).state$, + ); + if (decrypted?.length) { + return decrypted; + } + + if (folderData == null) { + return []; + } + + const folders = Object.values(folderData).map((f) => new Folder(f)); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); + if (!userKey) { + return []; + } + + const decryptFolderPromises = folders.map((f) => + f.decryptWithKey(userKey, this.encryptService), + ); + const decryptedFolders = await Promise.all(decryptFolderPromises); + decryptedFolders.sort(Utils.getSortFunction(this.i18nService, "name")); + + const noneFolder = new FolderView(); + noneFolder.name = this.i18nService.t("noneFolder"); + decryptedFolders.push(noneFolder); + + // Cache the decrypted folders + await this.setDecryptedFolders(decryptedFolders, userId); + return decryptedFolders; + } + + /** + * @returns a SingleUserState for the encrypted folders. + */ + private encryptedFoldersState(userId: UserId) { + return this.stateProvider.getUser(userId, FOLDER_ENCRYPTED_FOLDERS); + } + + /** + * Sets the decrypted folders state for a user. + * @param folders the decrypted folders + * @param userId the user id + */ + private async setDecryptedFolders(folders: FolderView[], userId: UserId): Promise { + await this.stateProvider.setUserState(FOLDER_DECRYPTED_FOLDERS, folders, userId); + } } diff --git a/libs/common/src/vault/services/key-state/ciphers.state.ts b/libs/common/src/vault/services/key-state/ciphers.state.ts index d9cedb89027..d85e625243e 100644 --- a/libs/common/src/vault/services/key-state/ciphers.state.ts +++ b/libs/common/src/vault/services/key-state/ciphers.state.ts @@ -28,6 +28,15 @@ export const DECRYPTED_CIPHERS = UserKeyDefinition.record( }, ); +export const FAILED_DECRYPTED_CIPHERS = UserKeyDefinition.array( + CIPHERS_MEMORY, + "failedDecryptedCiphers", + { + deserializer: (cipher: Jsonify) => CipherView.fromJSON(cipher), + clearOn: ["logout", "lock"], + }, +); + export const LOCAL_DATA_KEY = new UserKeyDefinition>( CIPHERS_DISK_LOCAL, "localData", diff --git a/libs/common/src/vault/services/key-state/folder.state.spec.ts b/libs/common/src/vault/services/key-state/folder.state.spec.ts index ece66b5d451..217a200ea88 100644 --- a/libs/common/src/vault/services/key-state/folder.state.spec.ts +++ b/libs/common/src/vault/services/key-state/folder.state.spec.ts @@ -1,11 +1,3 @@ -import { mock } from "jest-mock-extended"; - -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; -import { FolderService } from "../../abstractions/folder/folder.service.abstraction"; -import { FolderData } from "../../models/data/folder.data"; -import { Folder } from "../../models/domain/folder"; -import { FolderView } from "../../models/view/folder.view"; - import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "./folder.state"; describe("encrypted folders", () => { @@ -31,48 +23,32 @@ describe("encrypted folders", () => { }); describe("derived decrypted folders", () => { - const keyService = mock(); - const folderService = mock(); const sut = FOLDER_DECRYPTED_FOLDERS; - let data: FolderData; - beforeEach(() => { - data = { - id: "id", - name: "encName", - revisionDate: "2024-01-31T12:00:00.000Z", - }; + it("should deserialize decrypted folders", async () => { + const inputObj = [ + { + id: "id", + name: "encName", + revisionDate: "2024-01-31T12:00:00.000Z", + }, + ]; + + const expectedFolderView = [ + { + id: "id", + name: "encName", + revisionDate: new Date("2024-01-31T12:00:00.000Z"), + }, + ]; + + const result = sut.deserializer(JSON.parse(JSON.stringify(inputObj))); + + expect(result).toEqual(expectedFolderView); }); - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should deserialize encrypted folders", async () => { - const inputObj = [data]; - - const expectedFolderView = { - id: "id", - name: "encName", - revisionDate: new Date("2024-01-31T12:00:00.000Z"), - }; - - const result = sut.deserialize(JSON.parse(JSON.stringify(inputObj))); - - expect(result).toEqual([expectedFolderView]); - }); - - it("should derive encrypted folders", async () => { - const folderViewMock = new FolderView(new Folder(data)); - keyService.hasUserKey.mockResolvedValue(true); - folderService.decryptFolders.mockResolvedValue([folderViewMock]); - - const encryptedFoldersState = { id: data }; - const derivedStateResult = await sut.derive(encryptedFoldersState, { - folderService, - keyService, - }); - - expect(derivedStateResult).toEqual([folderViewMock]); + it("should handle null input", async () => { + const result = sut.deserializer(null); + expect(result).toEqual([]); }); }); diff --git a/libs/common/src/vault/services/key-state/folder.state.ts b/libs/common/src/vault/services/key-state/folder.state.ts index 7262d72d58e..99ad8e5ae35 100644 --- a/libs/common/src/vault/services/key-state/folder.state.ts +++ b/libs/common/src/vault/services/key-state/folder.state.ts @@ -1,34 +1,23 @@ import { Jsonify } from "type-fest"; -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; -import { DeriveDefinition, FOLDER_DISK, UserKeyDefinition } from "../../../platform/state"; -import { FolderService } from "../../abstractions/folder/folder.service.abstraction"; +import { FOLDER_DISK, FOLDER_MEMORY, UserKeyDefinition } from "../../../platform/state"; import { FolderData } from "../../models/data/folder.data"; -import { Folder } from "../../models/domain/folder"; import { FolderView } from "../../models/view/folder.view"; export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record( FOLDER_DISK, - "folders", + "folder", { deserializer: (obj: Jsonify) => FolderData.fromJSON(obj), clearOn: ["logout"], }, ); -export const FOLDER_DECRYPTED_FOLDERS = DeriveDefinition.from< - Record, - FolderView[], - { folderService: FolderService; keyService: KeyService } ->(FOLDER_ENCRYPTED_FOLDERS, { - deserializer: (obj) => obj.map((f) => FolderView.fromJSON(f)), - derive: async (from, { folderService, keyService }) => { - const folders = Object.values(from || {}).map((f) => new Folder(f)); - - if (await keyService.hasUserKey()) { - return await folderService.decryptFolders(folders); - } else { - return []; - } +export const FOLDER_DECRYPTED_FOLDERS = new UserKeyDefinition( + FOLDER_MEMORY, + "decryptedFolders", + { + deserializer: (obj: Jsonify) => obj?.map((f) => FolderView.fromJSON(f)) ?? [], + clearOn: ["logout", "lock"], }, -}); +); diff --git a/libs/common/src/vault/services/key-state/vault-settings.state.ts b/libs/common/src/vault/services/key-state/vault-settings.state.ts index 21364bbbf8e..35bb776cc96 100644 --- a/libs/common/src/vault/services/key-state/vault-settings.state.ts +++ b/libs/common/src/vault/services/key-state/vault-settings.state.ts @@ -25,3 +25,12 @@ export const SHOW_IDENTITIES_CURRENT_TAB = new UserKeyDefinition( clearOn: [], // do not clear user settings }, ); + +export const CLICK_ITEMS_AUTOFILL_VAULT_VIEW = new UserKeyDefinition( + VAULT_SETTINGS_DISK, + "clickItemsToAutofillOnVaultView", + { + deserializer: (obj) => obj, + clearOn: [], // do not clear user settings + }, +); diff --git a/libs/common/src/vault/services/vault-settings/vault-settings.service.ts b/libs/common/src/vault/services/vault-settings/vault-settings.service.ts index 39b96318217..85ab3914158 100644 --- a/libs/common/src/vault/services/vault-settings/vault-settings.service.ts +++ b/libs/common/src/vault/services/vault-settings/vault-settings.service.ts @@ -6,6 +6,7 @@ import { SHOW_CARDS_CURRENT_TAB, SHOW_IDENTITIES_CURRENT_TAB, USER_ENABLE_PASSKEYS, + CLICK_ITEMS_AUTOFILL_VAULT_VIEW, } from "../key-state/vault-settings.state"; /** @@ -39,6 +40,14 @@ export class VaultSettingsService implements VaultSettingsServiceAbstraction { readonly showIdentitiesCurrentTab$: Observable = this.showIdentitiesCurrentTabState.state$.pipe(map((x) => x ?? true)); + private clickItemsToAutofillVaultViewState: ActiveUserState = + this.stateProvider.getActive(CLICK_ITEMS_AUTOFILL_VAULT_VIEW); + /** + * {@link VaultSettingsServiceAbstraction.clickItemsToAutofillVaultView$$} + */ + readonly clickItemsToAutofillVaultView$: Observable = + this.clickItemsToAutofillVaultViewState.state$.pipe(map((x) => x ?? false)); + constructor(private stateProvider: StateProvider) {} /** @@ -55,6 +64,13 @@ export class VaultSettingsService implements VaultSettingsServiceAbstraction { await this.showIdentitiesCurrentTabState.update(() => value); } + /** + * {@link VaultSettingsServiceAbstraction.setClickItemsToAutofillVaultView} + */ + async setClickItemsToAutofillVaultView(value: boolean): Promise { + await this.clickItemsToAutofillVaultViewState.update(() => value); + } + /** * {@link VaultSettingsServiceAbstraction.setEnablePasskeys} */ diff --git a/libs/common/tsconfig.json b/libs/common/tsconfig.json index 27d8acbd360..2d1379f9c5f 100644 --- a/libs/common/tsconfig.json +++ b/libs/common/tsconfig.json @@ -1,5 +1,19 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/auth/common": ["../auth/src/common"], + // TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved + "@bitwarden/common/*": ["../common/src/*"], + // TODO: Remove once billing stops depending on components + "@bitwarden/components": ["../components/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + // TODO: Remove once billing stops depending on components + "@bitwarden/ui-common": ["../ui/common/src"] + } + }, "include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"], "exclude": ["node_modules", "dist"] } diff --git a/libs/components/src/a11y/a11y-grid.directive.ts b/libs/components/src/a11y/a11y-grid.directive.ts index c8e9d3d2fe3..ef7ba68b65c 100644 --- a/libs/components/src/a11y/a11y-grid.directive.ts +++ b/libs/components/src/a11y/a11y-grid.directive.ts @@ -86,6 +86,8 @@ export class A11yGridDirective implements AfterViewInit { }); this.getActiveCellContent().tabIndex = 0; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // eslint-disable-next-line no-console console.error("Unable to initialize grid"); diff --git a/libs/angular/src/directives/a11y-title.directive.ts b/libs/components/src/a11y/a11y-title.directive.ts similarity index 98% rename from libs/angular/src/directives/a11y-title.directive.ts rename to libs/components/src/a11y/a11y-title.directive.ts index f5f016b93c0..c3833f42ed2 100644 --- a/libs/angular/src/directives/a11y-title.directive.ts +++ b/libs/components/src/a11y/a11y-title.directive.ts @@ -4,6 +4,7 @@ import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; @Directive({ selector: "[appA11yTitle]", + standalone: true, }) export class A11yTitleDirective implements OnInit { @Input() set appA11yTitle(title: string) { diff --git a/libs/components/src/a11y/index.ts b/libs/components/src/a11y/index.ts new file mode 100644 index 00000000000..6090fb65d4e --- /dev/null +++ b/libs/components/src/a11y/index.ts @@ -0,0 +1 @@ +export * from "./a11y-title.directive"; diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index fb94e43b196..7ddb32da281 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -5,6 +5,8 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { delay, of } from "rxjs"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; import { ButtonModule } from "../button"; diff --git a/libs/components/src/avatar/avatar.stories.ts b/libs/components/src/avatar/avatar.stories.ts index d3a00fbe344..19a6f86d89c 100644 --- a/libs/components/src/avatar/avatar.stories.ts +++ b/libs/components/src/avatar/avatar.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16994", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26525&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/badge-list/badge-list.component.ts b/libs/components/src/badge-list/badge-list.component.ts index ac8cb3281ab..7d152761ed0 100644 --- a/libs/components/src/badge-list/badge-list.component.ts +++ b/libs/components/src/badge-list/badge-list.component.ts @@ -3,8 +3,9 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BadgeModule, BadgeVariant } from "../badge"; -import { I18nPipe } from "../shared/i18n.pipe"; @Component({ selector: "bit-badge-list", diff --git a/libs/components/src/badge-list/badge-list.stories.ts b/libs/components/src/badge-list/badge-list.stories.ts index 24024cb6bbd..f69ecde8377 100644 --- a/libs/components/src/badge-list/badge-list.stories.ts +++ b/libs/components/src/badge-list/badge-list.stories.ts @@ -13,8 +13,7 @@ export default { component: BadgeListComponent, decorators: [ moduleMetadata({ - imports: [SharedModule, BadgeModule], - declarations: [BadgeListComponent], + imports: [SharedModule, BadgeModule, BadgeListComponent], providers: [ { provide: I18nService, @@ -34,7 +33,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16956", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26440&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/badge/badge.stories.ts b/libs/components/src/badge/badge.stories.ts index 5d697f8ad8f..bff9eec6163 100644 --- a/libs/components/src/badge/badge.stories.ts +++ b/libs/components/src/badge/badge.stories.ts @@ -18,7 +18,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16956", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26440&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index d3f64329978..a7b710d6a74 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -3,8 +3,9 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; -import { I18nPipe } from "../shared/i18n.pipe"; type BannerTypes = "premium" | "info" | "warning" | "danger"; diff --git a/libs/components/src/banner/banner.stories.ts b/libs/components/src/banner/banner.stories.ts index c75d8b7034a..105d30bc04a 100644 --- a/libs/components/src/banner/banner.stories.ts +++ b/libs/components/src/banner/banner.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=2070%3A17207", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26720&t=b5tDKylm5sWm2yKo-4", }, }, args: { diff --git a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts index 9c8ccbccd3f..eb75a70ad75 100644 --- a/libs/components/src/breadcrumbs/breadcrumbs.stories.ts +++ b/libs/components/src/breadcrumbs/breadcrumbs.stories.ts @@ -35,6 +35,12 @@ export default { ], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-26962&t=b5tDKylm5sWm2yKo-4", + }, + }, args: { items: [], show: 3, diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index ed3dfc4e134..469c2d1b51b 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Meta, StoryObj } from "@storybook/angular"; import { ButtonComponent } from "./button.component"; @@ -15,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5115%3A26950", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28224&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; @@ -107,13 +105,13 @@ export const DisabledWithAttribute: Story = { }; export const Block: Story = { - render: (args: ButtonComponent) => ({ + render: (args) => ({ props: args, template: ` [block]="true" Link - + block Link diff --git a/libs/components/src/callout/callout.stories.ts b/libs/components/src/callout/callout.stories.ts index cb51e96e8da..3101d4316f1 100644 --- a/libs/components/src/callout/callout.stories.ts +++ b/libs/components/src/callout/callout.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17484", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28300&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts index b33f5f4a198..3482eedfd54 100644 --- a/libs/components/src/card/card.stories.ts +++ b/libs/components/src/card/card.stories.ts @@ -34,6 +34,12 @@ export default { (story) => `
    ${story}
    `, ), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-28355&t=b5tDKylm5sWm2yKo-4", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/checkbox/checkbox.stories.ts b/libs/components/src/checkbox/checkbox.stories.ts index c322f29b957..908085bb6df 100644 --- a/libs/components/src/checkbox/checkbox.stories.ts +++ b/libs/components/src/checkbox/checkbox.stories.ts @@ -9,6 +9,8 @@ import { } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; import { BadgeModule } from "../badge"; @@ -81,7 +83,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-35837&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index 5b07d0bbb96..a653d79f83f 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -18,6 +18,8 @@ import { import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { compareValues } from "../../../common/src/platform/misc/compare-values"; import { ButtonModule } from "../button"; import { IconButtonModule } from "../icon-button"; diff --git a/libs/components/src/chip-select/chip-select.stories.ts b/libs/components/src/chip-select/chip-select.stories.ts index 598fa5b80be..e9b78235ccb 100644 --- a/libs/components/src/chip-select/chip-select.stories.ts +++ b/libs/components/src/chip-select/chip-select.stories.ts @@ -30,6 +30,12 @@ export default { ], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-29548&t=b5tDKylm5sWm2yKo-4", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/color-password/color-password.stories.ts b/libs/components/src/color-password/color-password.stories.ts index 07418cad721..bb835d97d4a 100644 --- a/libs/components/src/color-password/color-password.stories.ts +++ b/libs/components/src/color-password/color-password.stories.ts @@ -14,7 +14,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/6fvTDa3zfvgWdizLQ7nSTP/Numbered-Password", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21540-46261&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/angular/src/directives/copy-click.directive.spec.ts b/libs/components/src/copy-click/copy-click.directive.spec.ts similarity index 78% rename from libs/angular/src/directives/copy-click.directive.spec.ts rename to libs/components/src/copy-click/copy-click.directive.spec.ts index 29466f7fbe3..eab616b141e 100644 --- a/libs/angular/src/directives/copy-click.directive.spec.ts +++ b/libs/components/src/copy-click/copy-click.directive.spec.ts @@ -3,23 +3,32 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; + +import { ToastService } from "../"; import { CopyClickDirective } from "./copy-click.directive"; @Component({ template: ` - - - - + + + + `, + standalone: true, + imports: [CopyClickDirective], }) class TestCopyClickComponent { - @ViewChild("noToast") noToastButton: ElementRef; - @ViewChild("infoToast") infoToastButton: ElementRef; - @ViewChild("successToast") successToastButton: ElementRef; - @ViewChild("toastWithLabel") toastWithLabelButton: ElementRef; + @ViewChild("noToast") noToastButton!: ElementRef; + @ViewChild("infoToast") infoToastButton!: ElementRef; + @ViewChild("successToast") successToastButton!: ElementRef; + @ViewChild("toastWithLabel") toastWithLabelButton!: ElementRef; } describe("CopyClickDirective", () => { @@ -32,7 +41,7 @@ describe("CopyClickDirective", () => { showToast.mockClear(); await TestBed.configureTestingModule({ - declarations: [CopyClickDirective, TestCopyClickComponent], + imports: [TestCopyClickComponent], providers: [ { provide: I18nService, diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/components/src/copy-click/copy-click.directive.ts similarity index 93% rename from libs/angular/src/directives/copy-click.directive.ts rename to libs/components/src/copy-click/copy-click.directive.ts index cb346ebf8d3..f91366360c5 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/components/src/copy-click/copy-click.directive.ts @@ -4,11 +4,12 @@ import { Directive, HostListener, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; -import { ToastVariant } from "@bitwarden/components/src/toast/toast.component"; + +import { ToastService, ToastVariant } from "../"; @Directive({ selector: "[appCopyClick]", + standalone: true, }) export class CopyClickDirective { private _showToast = false; diff --git a/libs/components/src/copy-click/index.ts b/libs/components/src/copy-click/index.ts new file mode 100644 index 00000000000..82b971f1f5c --- /dev/null +++ b/libs/components/src/copy-click/index.ts @@ -0,0 +1 @@ +export * from "./copy-click.directive"; diff --git a/libs/components/src/dialog/dialog.service.stories.ts b/libs/components/src/dialog/dialog.service.stories.ts index 5e938412804..e7c5a17c308 100644 --- a/libs/components/src/dialog/dialog.service.stories.ts +++ b/libs/components/src/dialog/dialog.service.stories.ts @@ -19,7 +19,7 @@ interface Animal { } @Component({ - template: ``, + template: ``, }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -42,8 +42,10 @@ class StoryDialogComponent { Animal: {{ animal }} - - + + `, @@ -91,7 +93,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-30495&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index ed47201805a..e9e3e898257 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -4,8 +4,9 @@ import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { CommonModule } from "@angular/common"; import { Component, HostBinding, Input } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitIconButtonComponent } from "../../icon-button/icon-button.component"; -import { I18nPipe } from "../../shared/i18n.pipe"; import { TypographyDirective } from "../../typography/typography.directive"; import { fadeIn } from "../animations"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index 1525d2e0171..03a88458f5a 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; @@ -70,7 +68,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-30495&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; @@ -78,7 +76,7 @@ export default { type Story = StoryObj; export const Default: Story = { - render: (args: DialogComponent) => ({ + render: (args) => ({ props: args, template: ` @@ -142,7 +140,7 @@ export const Loading: Story = { }; export const ScrollingContent: Story = { - render: (args: DialogComponent) => ({ + render: (args) => ({ props: args, template: ` @@ -197,7 +195,7 @@ export const TabContent: Story = { }; export const WithCards: Story = { - render: (args: DialogComponent) => ({ + render: (args) => ({ props: { formObj: new FormGroup({ name: new FormControl(""), diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts index 409d691beb0..b4bf199358b 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component } from "@angular/core"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; @@ -18,6 +16,7 @@ import { DialogModule } from "../../dialog.module";

    {{ group.title }}

    `, + template: ``, }) class StoryDialogComponent { constructor(public dialogService: DialogService) {} @@ -41,8 +41,10 @@ class StoryDialogComponent { Animal: {{ animal }} - - + + `, @@ -87,7 +89,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts index d86b56101b0..50016ef358d 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.stories.ts @@ -17,7 +17,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21514-19247&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts index 974589a667c..bb3680c1f3b 100644 --- a/libs/components/src/disclosure/disclosure.stories.ts +++ b/libs/components/src/disclosure/disclosure.stories.ts @@ -13,6 +13,12 @@ export default { imports: [DisclosureTriggerForDirective, DisclosureComponent, IconButtonModule], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-47329&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/drawer/drawer-body.component.ts b/libs/components/src/drawer/drawer-body.component.ts new file mode 100644 index 00000000000..9bd2adcffbc --- /dev/null +++ b/libs/components/src/drawer/drawer-body.component.ts @@ -0,0 +1,36 @@ +import { CdkScrollable } from "@angular/cdk/scrolling"; +import { ChangeDetectionStrategy, Component, Signal, inject } from "@angular/core"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { map } from "rxjs"; + +/** + * Body container for `bit-drawer` + */ +@Component({ + selector: "bit-drawer-body", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [], + host: { + class: + "tw-p-4 tw-pt-0 tw-block tw-overflow-auto tw-border-solid tw-border tw-border-transparent tw-transition-colors tw-duration-200", + "[class.tw-border-t-secondary-300]": "isScrolled()", + }, + hostDirectives: [ + { + directive: CdkScrollable, + }, + ], + template: ` `, +}) +export class DrawerBodyComponent { + private scrollable = inject(CdkScrollable); + + /** TODO: share this utility with browser popup header? */ + protected isScrolled: Signal = toSignal( + this.scrollable + .elementScrolled() + .pipe(map(() => this.scrollable.measureScrollOffset("top") > 0)), + { initialValue: false }, + ); +} diff --git a/libs/components/src/drawer/drawer-close.directive.ts b/libs/components/src/drawer/drawer-close.directive.ts new file mode 100644 index 00000000000..bf56dd8b71f --- /dev/null +++ b/libs/components/src/drawer/drawer-close.directive.ts @@ -0,0 +1,29 @@ +import { Directive, inject } from "@angular/core"; + +import { DrawerComponent } from "./drawer.component"; + +/** + * Closes the ancestor drawer + * + * @example + * + * ```html + * + * + * + * ``` + **/ +@Directive({ + selector: "button[bitDrawerClose]", + standalone: true, + host: { + "(click)": "onClick()", + }, +}) +export class DrawerCloseDirective { + private drawer = inject(DrawerComponent, { optional: true }); + + protected onClick() { + this.drawer?.open.set(false); + } +} diff --git a/libs/components/src/drawer/drawer-header.component.html b/libs/components/src/drawer/drawer-header.component.html new file mode 100644 index 00000000000..4652e5537ee --- /dev/null +++ b/libs/components/src/drawer/drawer-header.component.html @@ -0,0 +1,15 @@ +
    +
    + +

    + {{ title() }} +

    +
    + +
    diff --git a/libs/components/src/drawer/drawer-header.component.ts b/libs/components/src/drawer/drawer-header.component.ts new file mode 100644 index 00000000000..de112a448cf --- /dev/null +++ b/libs/components/src/drawer/drawer-header.component.ts @@ -0,0 +1,35 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, HostBinding, input } from "@angular/core"; + +import { I18nPipe } from "@bitwarden/ui-common"; + +import { IconButtonModule } from "../icon-button"; +import { TypographyModule } from "../typography"; + +import { DrawerCloseDirective } from "./drawer-close.directive"; + +/** + * Header container for `bit-drawer` + **/ +@Component({ + selector: "bit-drawer-header", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, DrawerCloseDirective, TypographyModule, IconButtonModule, I18nPipe], + templateUrl: "drawer-header.component.html", + host: { + class: "tw-block tw-pl-4 tw-pr-2 tw-py-2", + }, +}) +export class DrawerHeaderComponent { + /** + * The title to display + */ + title = input.required(); + + /** We don't want to set the HTML title attribute with `this.title` */ + @HostBinding("attr.title") + protected get getTitle(): null { + return null; + } +} diff --git a/libs/components/src/drawer/drawer-host.directive.ts b/libs/components/src/drawer/drawer-host.directive.ts new file mode 100644 index 00000000000..f5e3e56b099 --- /dev/null +++ b/libs/components/src/drawer/drawer-host.directive.ts @@ -0,0 +1,28 @@ +import { Portal } from "@angular/cdk/portal"; +import { Directive, signal } from "@angular/core"; + +/** + * Host that renders a drawer + * + * @internal + */ +@Directive({ + selector: "[bitDrawerHost]", + standalone: true, +}) +export class DrawerHostDirective { + private _portal = signal | undefined>(undefined); + + /** The portal to display */ + portal = this._portal.asReadonly(); + + open(portal: Portal) { + this._portal.set(portal); + } + + close(portal: Portal) { + if (portal === this.portal()) { + this._portal.set(undefined); + } + } +} diff --git a/libs/components/src/drawer/drawer.component.html b/libs/components/src/drawer/drawer.component.html new file mode 100644 index 00000000000..fce6b3c57eb --- /dev/null +++ b/libs/components/src/drawer/drawer.component.html @@ -0,0 +1,8 @@ + +
    + +
    +
    diff --git a/libs/components/src/drawer/drawer.component.ts b/libs/components/src/drawer/drawer.component.ts new file mode 100644 index 00000000000..ccabb6f0b6e --- /dev/null +++ b/libs/components/src/drawer/drawer.component.ts @@ -0,0 +1,76 @@ +import { CdkPortal, PortalModule } from "@angular/cdk/portal"; +import { CommonModule } from "@angular/common"; +import { + ChangeDetectionStrategy, + Component, + effect, + inject, + input, + model, + viewChild, +} from "@angular/core"; + +import { DrawerHostDirective } from "./drawer-host.directive"; + +/** + * A drawer is a panel of supplementary content that is adjacent to the page's main content. + * + * Drawers render in `bit-layout`. Drawers must be a descendant of `bit-layout`, but they do not need to be a direct descendant. + */ +@Component({ + selector: "bit-drawer", + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, PortalModule], + templateUrl: "drawer.component.html", +}) +export class DrawerComponent { + private drawerHost = inject(DrawerHostDirective); + private portal = viewChild.required(CdkPortal); + + /** + * Whether or not the drawer is open. + * + * Note: Does not support implicit boolean transform due to Angular limitation. Must be bound explicitly `[open]="true"` instead of just `open`. + * https://github.com/angular/angular/issues/55166#issuecomment-2032150999 + **/ + open = model(false); + + /** + * The ARIA role of the drawer. + * + * - [complementary](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/complementary_role) + * - For drawers that contain content that is complementary to the page's main content. (default) + * - [navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role) + * - For drawers that primary contain links to other content. + */ + role = input<"complementary" | "navigation">("complementary"); + + constructor() { + effect( + () => { + this.open() ? this.drawerHost.open(this.portal()) : this.drawerHost.close(this.portal()); + }, + { + allowSignalWrites: true, + }, + ); + + // Set `open` to `false` when another drawer is opened. + effect( + () => { + if (this.drawerHost.portal() !== this.portal()) { + this.open.set(false); + } + }, + { + allowSignalWrites: true, + }, + ); + } + + /** Toggle the drawer between open & closed */ + toggle() { + this.open.update((prev) => !prev); + } +} diff --git a/libs/components/src/drawer/drawer.mdx b/libs/components/src/drawer/drawer.mdx new file mode 100644 index 00000000000..0098ce64ea9 --- /dev/null +++ b/libs/components/src/drawer/drawer.mdx @@ -0,0 +1,120 @@ +import { Meta, Story, Primary, Controls } from "@storybook/addon-docs"; + +import * as stories from "./drawer.stories"; + +import { DrawerOpen as KitchenSink } from "../stories/kitchen-sink/kitchen-sink.stories"; + + + +```ts +import { DrawerComponent } from "@bitwarden/components"; +``` + +# Drawer + +A drawer is a panel of supplementary content that is adjacent to the page's main content. + + + + + +## Usage + +A `bit-drawer` in a template will not render inline, but rather will render adjacent to the main +page content. + +```html + + + +

    Lorem ipsum dolor...

    +
    +
    +``` + +`bit-drawer` must be a descendant of `bit-layout`, but it does not need to be a direct descendant. + +## Header and body + +Header and body content can be provided with the `bit-drawer-header` and `bit-drawer-body` +components, respectively. + +A title can be passed to the header by input: +`` + +Custom content can be rendered before the title with the header's `start` slot: + +```html + + + +``` + +## Opening and closing + +`bit-drawer` opens when its `open` input is `true`: + +```html +... +``` + +Note: Model inputs do not support implicit boolean transformation (see Angular reasoning +[here](https://github.com/angular/angular/issues/55166#issuecomment-2032150999)). `open` must be +bound explicitly `` instead of just ``. + +Buttons can be made to open/toggle drawers by referencing a template variable, or by manipulating +state that is bound to `open`: + +```html + ... +``` + +For convenience, close buttons can be created _inside_ the drawer with the `bitDrawerClose` +directive: + +```html + + + +``` + +## Multiple Drawers + +Only one drawer can be open at a time, and they do not stack. If a drawer is already open, opening +another will close and replace the one already open. + + + +## Headless + +Omitting `bit-drawer-header` and `bit-drawer-body` allows for fully customizable content. + + + +## Accessibility + +- The drawer should contain an h2 element. If you are using `bit-drawer-header`, this is created for + you via the `title` input: + +```html + +

    Hello world!

    +
    + + + + + + +``` + +- The ARIA role of the drawer can be set with the `role` attribute: + - [complementary](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/complementary_role) + (default) + - For drawers that contain content that is complementary to the page's main content. + - [navigation](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/navigation_role) + - For drawers that primary contain links to other content. + +## Kitchen Sink + + diff --git a/libs/components/src/drawer/drawer.module.ts b/libs/components/src/drawer/drawer.module.ts new file mode 100644 index 00000000000..9f51ba06b4e --- /dev/null +++ b/libs/components/src/drawer/drawer.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { DrawerBodyComponent } from "./drawer-body.component"; +import { DrawerCloseDirective } from "./drawer-close.directive"; +import { DrawerHeaderComponent } from "./drawer-header.component"; +import { DrawerComponent } from "./drawer.component"; + +@NgModule({ + imports: [DrawerComponent, DrawerHeaderComponent, DrawerBodyComponent, DrawerCloseDirective], + exports: [DrawerComponent, DrawerHeaderComponent, DrawerBodyComponent, DrawerCloseDirective], +}) +export class DrawerModule {} diff --git a/libs/components/src/drawer/drawer.stories.ts b/libs/components/src/drawer/drawer.stories.ts new file mode 100644 index 00000000000..54b4c89f4ce --- /dev/null +++ b/libs/components/src/drawer/drawer.stories.ts @@ -0,0 +1,124 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { RouterTestingModule } from "@angular/router/testing"; +import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { CalloutModule } from "../callout"; +import { LayoutComponent } from "../layout"; +import { mockLayoutI18n } from "../layout/mocks"; +import { + disableBothThemeDecorator, + positionFixedWrapperDecorator, +} from "../stories/storybook-decorators"; +import { TypographyModule } from "../typography"; +import { I18nMockService } from "../utils"; + +import { DrawerBodyComponent } from "./drawer-body.component"; +import { DrawerHeaderComponent } from "./drawer-header.component"; +import { DrawerComponent } from "./drawer.component"; +import { DrawerModule } from "./drawer.module"; + +export default { + title: "Component Library/Drawer", + component: DrawerComponent, + subcomponents: { + DrawerHeaderComponent, + DrawerBodyComponent, + }, + decorators: [ + positionFixedWrapperDecorator(), + disableBothThemeDecorator, + moduleMetadata({ + imports: [ + RouterTestingModule, + LayoutComponent, + DrawerModule, + ButtonModule, + CalloutModule, + TypographyModule, + ], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + ...mockLayoutI18n, + close: "Close", + }); + }, + }, + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

    The drawer is {{ open ? "open" : "closed" }}.

    + + + + + + + + +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +

    + +
    + + `, + }), + args: { + open: true, + }, +}; + +export const Headless: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

    The drawer is {{ open ? "open" : "closed" }}.

    + + +

    + Hello world! +
    + + `, + }), + args: { + open: true, + }, +}; + +export const MultipleDrawers: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + Foo + + + + Bar + + + `, + }), +}; diff --git a/libs/components/src/drawer/index.ts b/libs/components/src/drawer/index.ts new file mode 100644 index 00000000000..abf5b8d34f1 --- /dev/null +++ b/libs/components/src/drawer/index.ts @@ -0,0 +1,5 @@ +export * from "./drawer.module"; +export * from "./drawer.component"; +export * from "./drawer-body.component"; +export * from "./drawer-close.directive"; +export * from "./drawer-header.component"; diff --git a/libs/components/src/form-control/form-control.component.ts b/libs/components/src/form-control/form-control.component.ts index 9b87c44157a..d22d49ac03a 100644 --- a/libs/components/src/form-control/form-control.component.ts +++ b/libs/components/src/form-control/form-control.component.ts @@ -5,8 +5,8 @@ import { NgClass, NgIf } from "@angular/common"; import { Component, ContentChild, HostBinding, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "@bitwarden/ui-common"; -import { I18nPipe } from "../shared/i18n.pipe"; import { TypographyDirective } from "../typography/typography.directive"; import { BitFormControlAbstraction } from "./form-control.abstraction"; diff --git a/libs/components/src/form-field/bit-validators.stories.ts b/libs/components/src/form-field/bit-validators.stories.ts index df021256400..748f92f2523 100644 --- a/libs/components/src/form-field/bit-validators.stories.ts +++ b/libs/components/src/form-field/bit-validators.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FormsModule, ReactiveFormsModule, FormBuilder } from "@angular/forms"; import { StoryObj, Meta, moduleMetadata } from "@storybook/angular"; @@ -37,7 +35,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; @@ -51,7 +49,7 @@ const template = ` `; export const ForbiddenCharacters: StoryObj = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: { formObj: new FormBuilder().group({ name: ["", forbiddenCharacters(["\\", "/", "@", "#", "$", "%", "^", "&", "*", "(", ")"])], @@ -62,7 +60,7 @@ export const ForbiddenCharacters: StoryObj = { }; export const TrimValidator: StoryObj = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: { formObj: new FormBuilder().group({ name: [ diff --git a/libs/components/src/form-field/error-summary.component.ts b/libs/components/src/form-field/error-summary.component.ts index beed32a88ac..f9325d8f82a 100644 --- a/libs/components/src/form-field/error-summary.component.ts +++ b/libs/components/src/form-field/error-summary.component.ts @@ -4,7 +4,7 @@ import { NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; import { AbstractControl, UntypedFormGroup } from "@angular/forms"; -import { I18nPipe } from "../shared/i18n.pipe"; +import { I18nPipe } from "@bitwarden/ui-common"; @Component({ selector: "bit-error-summary", diff --git a/libs/components/src/form-field/error-summary.stories.ts b/libs/components/src/form-field/error-summary.stories.ts index 4e1031abaf6..4b1a30c45b3 100644 --- a/libs/components/src/form-field/error-summary.stories.ts +++ b/libs/components/src/form-field/error-summary.stories.ts @@ -34,7 +34,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 9f41c6cf6ac..4f7c2a67483 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -14,10 +14,11 @@ import { signal, } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitHintComponent } from "../form-control/hint.component"; import { BitLabel } from "../form-control/label.component"; import { inputBorderClasses } from "../input/input.directive"; -import { I18nPipe } from "../shared/i18n.pipe"; import { BitErrorComponent } from "./error.component"; import { BitFormFieldControl } from "./form-field-control"; diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index a02158655ee..738ac96bf76 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { TextFieldModule } from "@angular/cdk/text-field"; +import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; import { AbstractControl, UntypedFormBuilder, @@ -12,7 +13,6 @@ import { } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -import { A11yTitleDirective } from "@bitwarden/angular/src/directives/a11y-title.directive"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { AsyncActionsModule } from "../async-actions"; @@ -31,6 +31,41 @@ import { I18nMockService } from "../utils/i18n-mock.service"; import { BitFormFieldComponent } from "./form-field.component"; import { FormFieldModule } from "./form-field.module"; +// TOOD: This solves a circular dependency between components and angular. +@Directive({ + selector: "[appA11yTitle]", +}) +export class A11yTitleDirective implements OnInit { + @Input() set appA11yTitle(title: string) { + this.title = title; + this.setAttributes(); + } + + private title: string; + private originalTitle: string | null; + private originalAriaLabel: string | null; + + constructor( + private el: ElementRef, + private renderer: Renderer2, + ) {} + + ngOnInit() { + this.originalTitle = this.el.nativeElement.getAttribute("title"); + this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label"); + this.setAttributes(); + } + + private setAttributes() { + if (this.originalTitle === null) { + this.renderer.setAttribute(this.el.nativeElement, "title", this.title); + } + if (this.originalAriaLabel === null) { + this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title); + } + } +} + export default { title: "Component Library/Form/Field", component: BitFormFieldComponent, @@ -73,7 +108,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; @@ -190,7 +225,7 @@ export const Required: Story = { Label - + FormControl @@ -200,7 +235,7 @@ export const Required: Story = { }; export const Hint: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: { formObj: formObj, ...args, @@ -268,7 +303,7 @@ export const Readonly: Story = { Textarea Premium @@ -361,7 +396,7 @@ export const PartiallyDisabledButtonInputGroup: Story = { }; export const Select: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: args, template: /*html*/ ` @@ -377,7 +412,7 @@ export const Select: Story = { }; export const AdvancedSelect: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: args, template: /*html*/ ` @@ -422,7 +457,7 @@ export const FileInput: Story = { }; export const Textarea: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: args, template: /*html*/ ` diff --git a/libs/components/src/form-field/multi-select.stories.ts b/libs/components/src/form-field/multi-select.stories.ts index da4776ab025..8d84aa735bf 100644 --- a/libs/components/src/form-field/multi-select.stories.ts +++ b/libs/components/src/form-field/multi-select.stories.ts @@ -56,7 +56,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=5600%3A24278", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/form-field/password-input-toggle.stories.ts b/libs/components/src/form-field/password-input-toggle.stories.ts index 094f939e0ea..d46ec92ab37 100644 --- a/libs/components/src/form-field/password-input-toggle.stories.ts +++ b/libs/components/src/form-field/password-input-toggle.stories.ts @@ -27,7 +27,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, docs: { description: { diff --git a/libs/components/src/form/form.stories.ts b/libs/components/src/form/form.stories.ts index 23b2cc8cea2..6aef140fe5f 100644 --- a/libs/components/src/form/form.stories.ts +++ b/libs/components/src/form/form.stories.ts @@ -64,7 +64,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17689", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index 6274bb2b8d1..08c95c5d641 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -13,7 +13,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4369%3A16686", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-37011&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 54cdd7928cd..53454567b7f 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -6,6 +6,12 @@ import * as GenericIcons from "./icons"; export default { title: "Component Library/Icon", component: BitIconComponent, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-50335&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index a48750a99ff..7788f4986bf 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -1,3 +1,5 @@ +export { ButtonType } from "./shared/button-like.abstraction"; +export * from "./a11y"; export * from "./async-actions"; export * from "./avatar"; export * from "./badge-list"; @@ -5,15 +7,16 @@ export * from "./badge"; export * from "./banner"; export * from "./breadcrumbs"; export * from "./button"; -export { ButtonType } from "./shared/button-like.abstraction"; export * from "./callout"; export * from "./card"; export * from "./checkbox"; export * from "./chip-select"; export * from "./color-password"; export * from "./container"; +export * from "./copy-click"; export * from "./dialog"; export * from "./disclosure"; +export * from "./drawer"; export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; diff --git a/libs/components/src/item/item-action.component.ts b/libs/components/src/item/item-action.component.ts index ae8cb719724..a6ee3a34e6d 100644 --- a/libs/components/src/item/item-action.component.ts +++ b/libs/components/src/item/item-action.component.ts @@ -14,7 +14,7 @@ import { A11yCellDirective } from "../a11y/a11y-cell.directive"; * `top` and `bottom` units should be kept in sync with `item-content.component.ts`'s y-axis padding. * we want this `:after` element to be the same height as the `item-content` */ - "[&>button]:tw-relative [&>button:not([bit-item-content])]:after:tw-content-[''] [&>button]:after:tw-absolute [&>button]:after:tw-block bit-compact:[&>button]:after:tw-top-[-0.8rem] bit-compact:[&>button]:after:tw-bottom-[-0.8rem] [&>button]:after:tw-top-[-0.85rem] [&>button]:after:tw-bottom-[-0.85rem] [&>button]:after:tw-right-[-0.25rem] [&>button]:after:tw-left-[-0.25rem]", + "[&>button]:tw-relative [&>button:not([bit-item-content])]:after:tw-content-[''] [&>button]:after:tw-absolute [&>button]:after:tw-block bit-compact:[&>button]:after:tw-top-[-0.7rem] bit-compact:[&>button]:after:tw-bottom-[-0.7rem] [&>button]:after:tw-top-[-0.8rem] [&>button]:after:tw-bottom-[-0.80rem] [&>button]:after:tw-right-[-0.25rem] [&>button]:after:tw-left-[-0.25rem]", }, }) export class ItemActionComponent extends A11yCellDirective {} diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts index 833949ddb96..97a80484373 100644 --- a/libs/components/src/item/item.component.ts +++ b/libs/components/src/item/item.component.ts @@ -20,7 +20,7 @@ import { ItemActionComponent } from "./item-action.component"; providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], host: { class: - "tw-block tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg bit-compact:[&:not(bit-layout_*)]:tw-rounded-none bit-compact:[&:not(bit-layout_*)]:last-of-type:tw-rounded-b-lg bit-compact:[&:not(bit-layout_*)]:first-of-type:tw-rounded-t-lg tw-min-h-9 tw-mb-1.5 bit-compact:tw-mb-0", + "tw-block tw-box-border tw-overflow-hidden tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-cursor-pointer [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-100 tw-text-main tw-border-solid tw-border-b tw-border-0 [&:not(bit-layout_*)]:tw-rounded-lg bit-compact:[&:not(bit-layout_*)]:tw-rounded-none bit-compact:[&:not(bit-layout_*)]:last-of-type:tw-rounded-b-lg bit-compact:[&:not(bit-layout_*)]:first-of-type:tw-rounded-t-lg tw-min-h-9 tw-mb-1.5 bit-compact:tw-mb-0", }, }) export class ItemComponent extends A11yRowDirective { diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts index 5adf9d3c49d..3a64a334d0a 100644 --- a/libs/components/src/item/item.stories.ts +++ b/libs/components/src/item/item.stories.ts @@ -53,6 +53,12 @@ export default { }), componentWrapperDecorator((story) => `
    ${story}
    `), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-37011&t=b5tDKylm5sWm2yKo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/layout/layout.component.html b/libs/components/src/layout/layout.component.html index ccbb40c2b57..7c1c5b2501d 100644 --- a/libs/components/src/layout/layout.component.html +++ b/libs/components/src/layout/layout.component.html @@ -37,4 +37,5 @@ >
    +
    diff --git a/libs/components/src/layout/layout.component.ts b/libs/components/src/layout/layout.component.ts index d55ad8493eb..7bf8a6ad173 100644 --- a/libs/components/src/layout/layout.component.ts +++ b/libs/components/src/layout/layout.component.ts @@ -1,7 +1,9 @@ +import { PortalModule } from "@angular/cdk/portal"; import { CommonModule } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; import { RouterModule } from "@angular/router"; +import { DrawerHostDirective } from "../drawer/drawer-host.directive"; import { LinkModule } from "../link"; import { SideNavService } from "../navigation/side-nav.service"; import { SharedModule } from "../shared"; @@ -10,12 +12,14 @@ import { SharedModule } from "../shared"; selector: "bit-layout", templateUrl: "layout.component.html", standalone: true, - imports: [CommonModule, SharedModule, LinkModule, RouterModule], + imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule], + hostDirectives: [DrawerHostDirective], }) export class LayoutComponent { protected mainContentId = "main-content"; - constructor(protected sideNavService: SideNavService) {} + protected sideNavService = inject(SideNavService); + protected drawerPortal = inject(DrawerHostDirective).portal; focusMainContent() { document.getElementById(this.mainContentId)?.focus(); diff --git a/libs/components/src/layout/layout.stories.ts b/libs/components/src/layout/layout.stories.ts index a0eadebe7fa..e09055df596 100644 --- a/libs/components/src/layout/layout.stories.ts +++ b/libs/components/src/layout/layout.stories.ts @@ -6,10 +6,11 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { CalloutModule } from "../callout"; import { NavigationModule } from "../navigation"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { LayoutComponent } from "./layout.component"; +import { mockLayoutI18n } from "./mocks"; export default { title: "Component Library/Layout", @@ -22,12 +23,7 @@ export default { { provide: I18nService, useFactory: () => { - return new I18nMockService({ - toggleSideNavigation: "Toggle side navigation", - skipToContent: "Skip to content", - submenu: "submenu", - toggleCollapse: "toggle collapse", - }); + return new I18nMockService(mockLayoutI18n); }, }, ], @@ -35,6 +31,10 @@ export default { ], parameters: { chromatic: { viewports: [640, 1280] }, + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21662-51009&t=k6OTDDPZOTtypRqo-11", + }, }, } as Meta; diff --git a/libs/components/src/layout/mocks.ts b/libs/components/src/layout/mocks.ts new file mode 100644 index 00000000000..50c2bd9afb2 --- /dev/null +++ b/libs/components/src/layout/mocks.ts @@ -0,0 +1,7 @@ +/** TODO: create mock messages.json file for all of CL in favor of sharing per-Story mocks */ +export const mockLayoutI18n = { + toggleSideNavigation: "Toggle side navigation", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", +}; diff --git a/libs/components/src/link/link.stories.ts b/libs/components/src/link/link.stories.ts index cc3d26dc9d2..d07d33ae589 100644 --- a/libs/components/src/link/link.stories.ts +++ b/libs/components/src/link/link.stories.ts @@ -19,7 +19,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17419", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-39582&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index 786554e981c..96d430c5e6a 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -36,7 +36,7 @@ export class MenuTriggerForDirective implements OnDestroy { private defaultMenuConfig: OverlayConfig = { panelClass: "bit-menu-panel", hasBackdrop: true, - backdropClass: "cdk-overlay-transparent-backdrop", + backdropClass: ["cdk-overlay-transparent-backdrop", "bit-menu-panel-backdrop"], scrollStrategy: this.overlay.scrollStrategies.reposition(), positionStrategy: this.overlay .position() diff --git a/libs/components/src/menu/menu.stories.ts b/libs/components/src/menu/menu.stories.ts index 65fafd2d04d..f1f4d8df000 100644 --- a/libs/components/src/menu/menu.stories.ts +++ b/libs/components/src/menu/menu.stories.ts @@ -17,7 +17,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17952", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40144&t=b5tDKylm5sWm2yKo-11", }, }, } as Meta; diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index 53e51bfe2f9..71b00404cfb 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -24,10 +24,10 @@ import { import { NgSelectComponent, NgSelectModule } from "@ng-select/ng-select"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nPipe } from "@bitwarden/ui-common"; import { BadgeModule } from "../badge"; import { BitFormFieldControl } from "../form-field/form-field-control"; -import { I18nPipe } from "../shared/i18n.pipe"; import { SelectItemView } from "./models/select-item-view"; diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 58d93ddd3a4..37244f37c8d 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -12,8 +12,9 @@ import { SkipSelf, } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { IconButtonModule } from "../icon-button"; -import { I18nPipe } from "../shared/i18n.pipe"; import { NavBaseComponent } from "./nav-base.component"; import { NavGroupAbstraction, NavItemComponent } from "./nav-item.component"; @@ -70,6 +71,8 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI setOpen(isOpen: boolean) { this.open = isOpen; this.openChange.emit(this.open); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.open && this.parentNavGroup?.setOpen(this.open); } diff --git a/libs/components/src/navigation/nav-group.stories.ts b/libs/components/src/navigation/nav-group.stories.ts index a6fa53ff187..6fbb89d4d9e 100644 --- a/libs/components/src/navigation/nav-group.stories.ts +++ b/libs/components/src/navigation/nav-group.stories.ts @@ -6,8 +6,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LayoutComponent } from "../layout"; import { SharedModule } from "../shared/shared.module"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { NavGroupComponent } from "./nav-group.component"; import { NavigationModule } from "./navigation.module"; @@ -62,7 +62,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40145&t=b5tDKylm5sWm2yKo-4", }, chromatic: { viewports: [640, 1280] }, }, diff --git a/libs/components/src/navigation/nav-item.stories.ts b/libs/components/src/navigation/nav-item.stories.ts index 20d6ebd1d7e..0f6f406b2eb 100644 --- a/libs/components/src/navigation/nav-item.stories.ts +++ b/libs/components/src/navigation/nav-item.stories.ts @@ -5,8 +5,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { IconButtonModule } from "../icon-button"; import { LayoutComponent } from "../layout"; +import { positionFixedWrapperDecorator } from "../stories/storybook-decorators"; import { I18nMockService } from "../utils/i18n-mock.service"; -import { positionFixedWrapperDecorator } from "../utils/position-fixed-wrapper-decorator"; import { NavItemComponent } from "./nav-item.component"; import { NavigationModule } from "./navigation.module"; @@ -39,7 +39,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=4687%3A86642", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40145&t=b5tDKylm5sWm2yKo-4", }, chromatic: { viewports: [640, 1280] }, }, diff --git a/libs/components/src/navigation/side-nav.component.ts b/libs/components/src/navigation/side-nav.component.ts index c86a517100f..e8e4f131d6d 100644 --- a/libs/components/src/navigation/side-nav.component.ts +++ b/libs/components/src/navigation/side-nav.component.ts @@ -4,8 +4,9 @@ import { CdkTrapFocus } from "@angular/cdk/a11y"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Input, ViewChild } from "@angular/core"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitIconButtonComponent } from "../icon-button/icon-button.component"; -import { I18nPipe } from "../shared/i18n.pipe"; import { NavDividerComponent } from "./nav-divider.component"; import { SideNavService } from "./side-nav.service"; diff --git a/libs/components/src/no-items/no-items.stories.ts b/libs/components/src/no-items/no-items.stories.ts index d8e5b59bdbf..48d52476b17 100644 --- a/libs/components/src/no-items/no-items.stories.ts +++ b/libs/components/src/no-items/no-items.stories.ts @@ -13,6 +13,12 @@ export default { imports: [ButtonModule, NoItemsModule], }), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21665-25102&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/popover/popover.stories.ts b/libs/components/src/popover/popover.stories.ts index 10b7b248b79..fca4d659607 100644 --- a/libs/components/src/popover/popover.stories.ts +++ b/libs/components/src/popover/popover.stories.ts @@ -30,7 +30,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1717-15868", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40852&t=b5tDKylm5sWm2yKo-4", }, }, argTypes: { diff --git a/libs/components/src/progress/progress.stories.ts b/libs/components/src/progress/progress.stories.ts index 49a5398d2d9..1484dab0a21 100644 --- a/libs/components/src/progress/progress.stories.ts +++ b/libs/components/src/progress/progress.stories.ts @@ -8,7 +8,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A18185&t=AM0acaIJ00BUhZKz-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-40933&t=b5tDKylm5sWm2yKo-4", }, }, args: { diff --git a/libs/components/src/radio-button/radio-button.stories.ts b/libs/components/src/radio-button/radio-button.stories.ts index dcef13a19be..f5d7f6732f5 100644 --- a/libs/components/src/radio-button/radio-button.stories.ts +++ b/libs/components/src/radio-button/radio-button.stories.ts @@ -31,7 +31,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-35836&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/radio-button/radio-group.component.ts b/libs/components/src/radio-button/radio-group.component.ts index b9e48f46445..4ab626f7964 100644 --- a/libs/components/src/radio-button/radio-group.component.ts +++ b/libs/components/src/radio-button/radio-group.component.ts @@ -4,8 +4,9 @@ import { NgIf, NgTemplateOutlet } from "@angular/common"; import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core"; import { ControlValueAccessor, NgControl, Validators } from "@angular/forms"; +import { I18nPipe } from "@bitwarden/ui-common"; + import { BitLabel } from "../form-control/label.component"; -import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; diff --git a/libs/components/src/search/search.component.html b/libs/components/src/search/search.component.html index b5f9fe94eda..5bb25425e57 100644 --- a/libs/components/src/search/search.component.html +++ b/libs/components/src/search/search.component.html @@ -18,5 +18,6 @@ (ngModelChange)="onChange($event)" (blur)="onTouch()" [disabled]="disabled" + [attr.autocomplete]="autocomplete" /> diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index 6ec79eaa84e..7f1bd781e9d 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -9,10 +9,10 @@ import { } from "@angular/forms"; import { isBrowserSafariApi } from "@bitwarden/platform"; +import { I18nPipe } from "@bitwarden/ui-common"; import { InputModule } from "../input/input.module"; import { FocusableElement } from "../shared/focusable-element"; -import { I18nPipe } from "../shared/i18n.pipe"; let nextId = 0; @@ -46,6 +46,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { @Input() disabled: boolean; @Input() placeholder: string; + @Input() autocomplete: string; getFocusTarget() { return this.input.nativeElement; diff --git a/libs/components/src/search/search.stories.ts b/libs/components/src/search/search.stories.ts index 71c180c6d51..a6cd714d43a 100644 --- a/libs/components/src/search/search.stories.ts +++ b/libs/components/src/search/search.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; @@ -34,7 +32,7 @@ export default { type Story = StoryObj; export const Default: Story = { - render: (args: SearchComponent) => ({ + render: (args) => ({ props: args, template: ` diff --git a/libs/components/src/section/section-header.component.html b/libs/components/src/section/section-header.component.html index 3f96e22540f..f070cfeae02 100644 --- a/libs/components/src/section/section-header.component.html +++ b/libs/components/src/section/section-header.component.html @@ -2,7 +2,7 @@
    -
    +
    diff --git a/libs/components/src/section/section.stories.ts b/libs/components/src/section/section.stories.ts index 0d36d6e5a11..53e6bc078c5 100644 --- a/libs/components/src/section/section.stories.ts +++ b/libs/components/src/section/section.stories.ts @@ -22,6 +22,12 @@ export default { }), componentWrapperDecorator((story) => `
    ${story}
    `), ], + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=21666-19363&t=k6OTDDPZOTtypRqo-11", + }, + }, } as Meta; type Story = StoryObj; diff --git a/libs/components/src/select/select.stories.ts b/libs/components/src/select/select.stories.ts index 4bc85d8dba2..c030bea86c5 100644 --- a/libs/components/src/select/select.stories.ts +++ b/libs/components/src/select/select.stories.ts @@ -33,7 +33,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/3tWtMSYoLB0ZLEimLNzYsm/End-user-%26-admin-Vault-Refresh?t=7QEmGA69YTOF8sXU-0", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=13213-55392&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/shared/shared.module.ts b/libs/components/src/shared/shared.module.ts index 253b049f8fe..99d052c3350 100644 --- a/libs/components/src/shared/shared.module.ts +++ b/libs/components/src/shared/shared.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { I18nPipe } from "./i18n.pipe"; +import { I18nPipe } from "@bitwarden/ui-common"; @NgModule({ imports: [CommonModule, I18nPipe], diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts index a867d9cdf53..7709506f050 100644 --- a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts @@ -10,7 +10,7 @@ import { TableDataSource, TableModule } from "../../../table"; selector: "dialog-virtual-scroll-block", standalone: true, imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule], - template: ` + template: /*html*/ ` diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts index 687b8917381..568c78566f6 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -1,5 +1,5 @@ import { DialogRef } from "@angular/cdk/dialog"; -import { Component } from "@angular/core"; +import { Component, signal } from "@angular/core"; import { DialogService } from "../../../dialog"; import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; @@ -15,8 +15,8 @@ import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; Dialog body text goes here. - - + + `, @@ -28,13 +28,7 @@ class KitchenSinkDialog { @Component({ standalone: true, selector: "bit-tab-main", - imports: [ - KitchenSinkSharedModule, - KitchenSinkTable, - KitchenSinkToggleList, - KitchenSinkForm, - KitchenSinkDialog, - ], + imports: [KitchenSinkSharedModule, KitchenSinkTable, KitchenSinkToggleList, KitchenSinkForm], template: ` Kitchen Sink test zone @@ -48,6 +42,11 @@ class KitchenSinkDialog {

    +
    +

    Bitwarden Kitchen Sink

    + Learn more +
    +

    The purpose of this story is to compose together all of our components. When snapshot tests @@ -63,18 +62,14 @@ class KitchenSinkDialog {

    -
    -

    Bitwarden

    - Learn more -
    -

    About

    - + +

    Companies using Bitwarden

    @@ -99,15 +94,87 @@ class KitchenSinkDialog {
    + + + + +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

    + + What did foo say to bar? + + +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

    +

    + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt + ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation + ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur + sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. +

    +
    +
    `, }) export class KitchenSinkMainComponent { constructor(public dialogService: DialogService) {} - openDefaultDialog() { + protected drawerOpen = signal(false); + + openDialog() { this.dialogService.open(KitchenSinkDialog); } + openDrawer() { + this.drawerOpen.set(true); + } + navItems = [ { icon: "bwi-collection", name: "Password Managers", route: "/" }, { icon: "bwi-collection", name: "Favorites", route: "/" }, diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts index 3c6d6f11444..ba71483d7de 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts @@ -20,7 +20,11 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Password Manager Everyone - + Anchor link Another link @@ -33,7 +37,11 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; Secrets Manager Developers - + Anchor link Another link diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts index 2804c9e8351..6f0054912cf 100644 --- a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts @@ -7,7 +7,7 @@ import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; selector: "bit-kitchen-sink-toggle-list", imports: [KitchenSinkSharedModule], template: ` -
    +
    All 3 diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts index 56e3a92e2a3..c4fe2f9b2af 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts @@ -13,6 +13,7 @@ import { CalloutModule } from "../../callout"; import { CheckboxModule } from "../../checkbox"; import { ColorPasswordModule } from "../../color-password"; import { DialogModule } from "../../dialog"; +import { DrawerModule } from "../../drawer"; import { FormControlModule } from "../../form-control"; import { FormFieldModule } from "../../form-field"; import { IconModule } from "../../icon"; @@ -48,6 +49,7 @@ import { TypographyModule } from "../../typography"; ColorPasswordModule, CommonModule, DialogModule, + DrawerModule, FormControlModule, FormFieldModule, FormsModule, @@ -85,6 +87,7 @@ import { TypographyModule } from "../../typography"; ColorPasswordModule, CommonModule, DialogModule, + DrawerModule, FormControlModule, FormFieldModule, FormsModule, diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index 203c510f814..62b93984384 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -1,13 +1,7 @@ import { importProvidersFrom } from "@angular/core"; import { provideNoopAnimations } from "@angular/platform-browser/animations"; import { RouterModule } from "@angular/router"; -import { - Meta, - StoryObj, - applicationConfig, - componentWrapperDecorator, - moduleMetadata, -} from "@storybook/angular"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; import { userEvent, getAllByRole, @@ -23,6 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { DialogService } from "../../dialog"; import { LayoutComponent } from "../../layout"; import { I18nMockService } from "../../utils/i18n-mock.service"; +import { disableBothThemeDecorator, positionFixedWrapperDecorator } from "../storybook-decorators"; import { DialogVirtualScrollBlockComponent } from "./components/dialog-virtual-scroll-block.component"; import { KitchenSinkForm } from "./components/kitchen-sink-form.component"; @@ -35,25 +30,8 @@ export default { title: "Documentation / Kitchen Sink", component: LayoutComponent, decorators: [ - componentWrapperDecorator( - /** - * Applying a CSS transform makes a `position: fixed` element act like it is `position: relative` - * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 - */ - (story) => { - return /* HTML */ `
    - ${story} -
    `; - }, - ({ globals }) => { - /** - * avoid a bug with the way that we render the same component twice in the same iframe and how - * that interacts with the router-outlet - */ - const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; - return { theme: themeOverride }; - }, - ), + positionFixedWrapperDecorator(), + disableBothThemeDecorator, moduleMetadata({ imports: [ KitchenSinkSharedModule, @@ -130,9 +108,12 @@ export const MenuOpen: Story = { const menuButton = getAllByRole(table, "button")[0]; await userEvent.click(menuButton); }, + parameters: { + chromatic: { ignoreSelectors: [".bit-menu-panel-backdrop"] }, + }, }; -export const DefaultDialogOpen: Story = { +export const DialogOpen: Story = { ...Default, play: async (context) => { const canvas = context.canvasElement; @@ -145,6 +126,19 @@ export const DefaultDialogOpen: Story = { }, }; +export const DrawerOpen: Story = { + ...Default, + play: async (context) => { + const canvas = context.canvasElement; + const drawerButton = getByRole(canvas, "button", { + name: "Open Drawer", + }); + + // workaround for userEvent not firing in FF https://github.com/testing-library/user-event/issues/1075 + await fireEvent.click(drawerButton); + }, +}; + export const PopoverOpen: Story = { ...Default, play: async (context) => { @@ -193,4 +187,10 @@ export const VirtualScrollBlockingDialog: Story = { await userEvent.click(dialogButton); }, + parameters: { + chromatic: { + // TODO CL-524 fix flaky story (number of virtual scroll rows is inconsistent) + disableSnapshot: true, + }, + }, }; diff --git a/libs/components/src/utils/position-fixed-wrapper-decorator.ts b/libs/components/src/stories/storybook-decorators.ts similarity index 50% rename from libs/components/src/utils/position-fixed-wrapper-decorator.ts rename to libs/components/src/stories/storybook-decorators.ts index a3298e6ad03..d59f2dd1f3e 100644 --- a/libs/components/src/utils/position-fixed-wrapper-decorator.ts +++ b/libs/components/src/stories/storybook-decorators.ts @@ -11,7 +11,21 @@ export const positionFixedWrapperDecorator = (wrapper?: (story: string) => strin * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 */ (story) => - /* HTML */ `
    + /* HTML */ `
    ${wrapper ? wrapper(story) : story}
    `, ); + +export const disableBothThemeDecorator = componentWrapperDecorator( + (story) => story, + ({ globals }) => { + /** + * avoid a bug with the way that we render the same component twice in the same iframe and how + * that interacts with the router-outlet + */ + const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; + return { theme: themeOverride }; + }, +); diff --git a/libs/components/src/table/sortable.component.ts b/libs/components/src/table/sortable.component.ts index d3309c03aa9..bdfb87ac52f 100644 --- a/libs/components/src/table/sortable.component.ts +++ b/libs/components/src/table/sortable.component.ts @@ -10,7 +10,12 @@ import { TableComponent } from "./table.component"; @Component({ selector: "th[bitSortable]", template: ` - diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index 4ebc3045d13..e8ab24ee8b7 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -21,7 +21,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A18371", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41282&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/tabs/tabs.stories.ts b/libs/components/src/tabs/tabs.stories.ts index 6b460d8ee00..250a7443065 100644 --- a/libs/components/src/tabs/tabs.stories.ts +++ b/libs/components/src/tabs/tabs.stories.ts @@ -74,7 +74,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17922", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41432&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/src/toast/index.ts b/libs/components/src/toast/index.ts index f0b55402194..5afed111c0f 100644 --- a/libs/components/src/toast/index.ts +++ b/libs/components/src/toast/index.ts @@ -1,2 +1,3 @@ export * from "./toast.module"; export * from "./toast.service"; +export type { ToastVariant } from "./toast.component"; diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts index 2ca1c0fa952..abb737f5c23 100644 --- a/libs/components/src/toast/toast.stories.ts +++ b/libs/components/src/toast/toast.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @@ -24,7 +22,7 @@ const toastServiceExampleTemplate = ` }) export class ToastServiceExampleComponent { @Input() - toastOptions: ToastOptions; + toastOptions?: ToastOptions; constructor(protected toastService: ToastService) {} } @@ -40,7 +38,7 @@ export default { }), applicationConfig({ providers: [ - ToastModule.forRoot().providers, + ToastModule.forRoot().providers!, { provide: I18nService, useFactory: () => { @@ -65,7 +63,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=16329-41506&t=b5tDKylm5sWm2yKo-11", }, }, } as Meta; diff --git a/libs/components/src/toggle-group/toggle-group.stories.ts b/libs/components/src/toggle-group/toggle-group.stories.ts index fc8ea0ea929..4860636c159 100644 --- a/libs/components/src/toggle-group/toggle-group.stories.ts +++ b/libs/components/src/toggle-group/toggle-group.stories.ts @@ -19,7 +19,7 @@ export default { parameters: { design: { type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A17157", + url: "https://www.figma.com/design/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881-17157&t=b5tDKylm5sWm2yKo-4", }, }, } as Meta; diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 26616d07156..6e887030c34 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -11,6 +11,7 @@ module.exports = { content: [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", + "../../libs/key-management/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", ], safelist: [], diff --git a/libs/components/tsconfig.json b/libs/components/tsconfig.json index dabcecf78e9..abd5830d425 100644 --- a/libs/components/tsconfig.json +++ b/libs/components/tsconfig.json @@ -20,8 +20,8 @@ "lib": ["es2020", "dom"], "paths": { "@bitwarden/common/*": ["../common/src/*"], - "@bitwarden/angular/*": ["../angular/src/*"], - "@bitwarden/platform": ["../platform/src"] + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"] }, "plugins": [ { diff --git a/libs/importer/jest.config.js b/libs/importer/jest.config.js index 68daba3d407..ab449dc7757 100644 --- a/libs/importer/jest.config.js +++ b/libs/importer/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../shared/jest.config.ts"); diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 0da8127369e..c53a1d3f522 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -395,6 +395,11 @@ Open the FullClient, go to the Main Menu and select Export. Start the export passwords wizard and follow the instructions to export a CSV file. + + Log in to NordPass and open Settings → Scroll down to the Import and Export section + and select Export items → Enter your Master Password and select Continue. → Save + the CSV file on your device. + {{ "selectImportFile" | i18n }} -
    +
    diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 0035fbdf10d..f2bf7471d44 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -16,7 +16,7 @@ import { import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; import * as JSZip from "jszip"; import { concat, Observable, Subject, lastValueFrom, combineLatest, firstValueFrom } from "rxjs"; -import { filter, map, takeUntil } from "rxjs/operators"; +import { filter, map, switchMap, takeUntil } from "rxjs/operators"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -153,6 +153,8 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { private _importBlockedByPolicy = false; protected isFromAC = false; + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + formGroup = this.formBuilder.group({ vaultSelector: [ "myVault", @@ -206,6 +208,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { @Optional() protected importCollectionService: ImportCollectionServiceAbstraction, protected toastService: ToastService, + protected accountService: AccountService, ) {} protected get importBlockedByPolicy(): boolean { @@ -257,7 +260,10 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { private handleImportInit() { // Filter out the no folder-item from folderViews$ - this.folders$ = this.folderService.folderViews$.pipe( + this.folders$ = this.activeUserId$.pipe( + switchMap((userId) => { + return this.folderService.folderViews$(userId); + }), map((folders) => folders.filter((f) => f.id != null)), ); diff --git a/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts b/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts index 368a9b321be..62826ae3fe9 100644 --- a/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts +++ b/libs/importer/src/components/lastpass/lastpass-direct-import.service.ts @@ -12,9 +12,9 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { DialogService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { DialogService } from "../../../../components/src/dialog"; import { ClientInfo, Vault } from "../../importers/lastpass/access"; import { FederatedUserContext } from "../../importers/lastpass/access/models"; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index 8b497beac93..e8f63ef8705 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -267,7 +267,7 @@ describe("ImportService", () => { function createCipher(options: Partial = {}) { const cipher = new CipherView(); - cipher.name; + cipher.name = options.name; cipher.type = options.type; cipher.folderId = options.folderId; cipher.collectionIds = options.collectionIds; diff --git a/libs/importer/tsconfig.json b/libs/importer/tsconfig.json index 6004a56fb55..09eb33e2884 100644 --- a/libs/importer/tsconfig.json +++ b/libs/importer/tsconfig.json @@ -1,5 +1,22 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/key-management/jest.config.js b/libs/key-management/jest.config.js index e20d02303d9..ad8023e906b 100644 --- a/libs/key-management/jest.config.js +++ b/libs/key-management/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index f5ab624bb22..da59c2986b2 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -5,13 +5,29 @@ import { Observable } from "rxjs"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; import { KdfConfig } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-organization.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileProviderOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-provider-organization.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileProviderResponse } from "../../../common/src/admin-console/models/response/profile-provider.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeySuffixOptions, HashPurpose } from "../../../common/src/platform/enums"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SymmetricCryptoKey } from "../../../common/src/platform/models/domain/symmetric-crypto-key"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { OrganizationId, UserId } from "../../../common/src/types/guid"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserKey, MasterKey, diff --git a/libs/key-management/src/angular/index.ts b/libs/key-management/src/angular/index.ts new file mode 100644 index 00000000000..1eb9b88b072 --- /dev/null +++ b/libs/key-management/src/angular/index.ts @@ -0,0 +1,6 @@ +/** + * This barrel file should only contain Angular exports + */ + +export { LockComponent } from "./lock/components/lock.component"; +export { LockComponentService, UnlockOptions } from "./lock/services/lock-component.service"; diff --git a/libs/auth/src/angular/lock/lock.component.html b/libs/key-management/src/angular/lock/components/lock.component.html similarity index 94% rename from libs/auth/src/angular/lock/lock.component.html rename to libs/key-management/src/angular/lock/components/lock.component.html index 5f5991c681e..437e29447e2 100644 --- a/libs/auth/src/angular/lock/lock.component.html +++ b/libs/key-management/src/angular/lock/components/lock.component.html @@ -6,15 +6,13 @@ - + + + diff --git a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts index cec818b1cd6..7bcffd92399 100644 --- a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -34,6 +35,7 @@ export class CredentialGeneratorHistoryDialogComponent { private accountService: AccountService, private history: GeneratorHistoryService, private dialogService: DialogService, + private dialogRef: DialogRef, ) { this.accountService.activeAccount$ .pipe( @@ -52,7 +54,13 @@ export class CredentialGeneratorHistoryDialogComponent { .subscribe(this.hasHistory$); } - clear = async () => { + /** closes the dialog */ + protected close() { + this.dialogRef.close(); + } + + /** Launches clear history flow */ + protected async clear() { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "clearGeneratorHistoryTitle" }, content: { key: "cleargGeneratorHistoryDescription" }, @@ -64,5 +72,5 @@ export class CredentialGeneratorHistoryDialogComponent { if (confirmed) { await this.history.clear(await firstValueFrom(this.userId$)); } - }; + } } diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index ce86abe80ae..6f27c7020db 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -56,7 +56,7 @@
    - + {{ "type" | i18n }} -
    + {{ "service" | i18n }} + {{ "forwarderDomainName" | i18n }}
    {{ "options" | i18n }}
    - +
    diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index 7e230d8bfd6..85363412ffa 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -19,8 +19,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { Option } from "@bitwarden/components/src/select/option"; +import { ToastService, Option } from "@bitwarden/components"; import { CredentialGeneratorService, Generators, @@ -94,6 +93,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { @Output() readonly onGenerated = new EventEmitter(); + /** emits algorithm info when the selected algorithm changes */ + @Output() + readonly onAlgorithm = new EventEmitter(); + async ngOnInit() { if (this.userId) { this.userId$.next(this.userId); @@ -186,6 +189,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // template bindings refresh immediately this.zone.run(() => { this.algorithm$.next(algorithm); + this.onAlgorithm.next(algorithm); }); }); diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index 9f8e00921fb..5e4d1079725 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -2,7 +2,7 @@

    {{ "options" | i18n }}

    - +
    diff --git a/libs/tools/generator/components/src/subaddress-settings.component.html b/libs/tools/generator/components/src/subaddress-settings.component.html index 1dfb5e3460d..b7f71b12b2a 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.html +++ b/libs/tools/generator/components/src/subaddress-settings.component.html @@ -1,4 +1,4 @@ - + {{ "email" | i18n }}
    - + {{ "type" | i18n }} -
    + {{ "service" | i18n }} (); + /** emits algorithm info when the selected algorithm changes */ + @Output() + readonly onAlgorithm = new EventEmitter(); + /** Removes bottom margin from internal elements */ @Input({ transform: coerceBooleanProperty }) disableMargin = false; @@ -248,6 +251,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // template bindings refresh immediately this.zone.run(() => { this.algorithm$.next(algorithm); + this.onAlgorithm.next(algorithm); }); }); diff --git a/libs/tools/generator/components/src/username-settings.component.html b/libs/tools/generator/components/src/username-settings.component.html index 649cd052e7b..fd8b32e2b3e 100644 --- a/libs/tools/generator/components/src/username-settings.component.html +++ b/libs/tools/generator/components/src/username-settings.component.html @@ -1,4 +1,4 @@ - + (); + +describe("email - catchall generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(catchall.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = catchall.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: CatchallGenerationOptions = { + catchallType: "random", + catchallDomain: "example.com", + }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a catchall constraints", () => { + const context = { defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(CatchallConstraints); + }); + + it("extracts the domain from context.email", () => { + const context = { email: "foo@example.com", defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context) as CatchallConstraints; + + expect(constraints.domain).toEqual("example.com"); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/email/catchall.ts b/libs/tools/generator/core/src/metadata/email/catchall.ts new file mode 100644 index 00000000000..0711e5c3719 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/catchall.ts @@ -0,0 +1,70 @@ +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { EmailRandomizer } from "../../engine"; +import { CatchallConstraints } from "../../policies/catchall-constraints"; +import { + CatchallGenerationOptions, + CredentialGenerator, + GeneratorDependencyProvider, +} from "../../types"; +import { Algorithm, Type, Profile } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const catchall: GeneratorMetadata = deepFreeze({ + id: Algorithm.catchall, + category: Type.email, + weight: 210, + i18nKeys: { + name: "catchallEmail", + description: "catchallEmailDesc", + credentialType: "email", + generateCredential: "generateEmail", + credentialGenerated: "emailGenerated", + copyCredential: "copyEmail", + useCredential: "useThisEmail", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "catchallGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "catchallType", + "catchallDomain", + ]), + state: GENERATOR_DISK, + initial: { + catchallType: "random", + catchallDomain: "", + }, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + }, + constraints: { + default: { catchallDomain: { minLength: 1 } }, + create(_policies, context) { + return new CatchallConstraints(context.email ?? ""); + }, + }, + }, + }, +}); + +export default catchall; diff --git a/libs/tools/generator/core/src/metadata/email/forwarder.ts b/libs/tools/generator/core/src/metadata/email/forwarder.ts new file mode 100644 index 00000000000..1dfc219d466 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/forwarder.ts @@ -0,0 +1,4 @@ +// Forwarders are pending integration with the extension API +// +// They use the 300-block of weights and derive their metadata +// using logic similar to `toCredentialGeneratorConfiguration` diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts new file mode 100644 index 00000000000..2ac7645ed30 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/plus-address.spec.ts @@ -0,0 +1,65 @@ +import { mock } from "jest-mock-extended"; + +import { EmailRandomizer } from "../../engine"; +import { SubaddressConstraints } from "../../policies/subaddress-constraints"; +import { SubaddressGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import plusAddress from "./plus-address"; + +const dependencyProvider = mock(); + +describe("email - plus address generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(plusAddress.engine.create(dependencyProvider)).toBeInstanceOf(EmailRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = plusAddress.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: SubaddressGenerationOptions = { + subaddressType: "random", + subaddressEmail: "foo@example.com", + }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a subaddress constraints", () => { + const context = { defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(SubaddressConstraints); + }); + + it("sets the constraint email to context.email", () => { + const context = { email: "bar@example.com", defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context) as SubaddressConstraints; + + expect(constraints.email).toEqual("bar@example.com"); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/email/plus-address.ts b/libs/tools/generator/core/src/metadata/email/plus-address.ts new file mode 100644 index 00000000000..0db0acd415c --- /dev/null +++ b/libs/tools/generator/core/src/metadata/email/plus-address.ts @@ -0,0 +1,72 @@ +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { EmailRandomizer } from "../../engine"; +import { SubaddressConstraints } from "../../policies/subaddress-constraints"; +import { + CredentialGenerator, + GeneratorDependencyProvider, + SubaddressGenerationOptions, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const plusAddress: GeneratorMetadata = deepFreeze({ + id: Algorithm.plusAddress, + category: Type.email, + weight: 200, + i18nKeys: { + name: "plusAddressedEmail", + description: "plusAddressedEmailDesc", + credentialType: "email", + generateCredential: "generateEmail", + credentialGenerated: "emailGenerated", + copyCredential: "copyEmail", + useCredential: "useThisEmail", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "subaddressGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "subaddressType", + "subaddressEmail", + ]), + state: GENERATOR_DISK, + initial: { + subaddressType: "random", + subaddressEmail: "", + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + }, + constraints: { + default: {}, + create(_policy, context) { + return new SubaddressConstraints(context.email ?? ""); + }, + }, + }, + }, +}); + +export default plusAddress; diff --git a/libs/tools/generator/core/src/metadata/generator-metadata.ts b/libs/tools/generator/core/src/metadata/generator-metadata.ts new file mode 100644 index 00000000000..9296d30430e --- /dev/null +++ b/libs/tools/generator/core/src/metadata/generator-metadata.ts @@ -0,0 +1,29 @@ +import { CredentialGenerator, GeneratorDependencyProvider } from "../types"; + +import { AlgorithmMetadata } from "./algorithm-metadata"; +import { Profile } from "./data"; +import { ProfileMetadata } from "./profile-metadata"; + +/** Extends the algorithm metadata with storage and engine configurations. + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; + */ +export type GeneratorMetadata = AlgorithmMetadata & { + /** An algorithm that generates credentials when ran. */ + engine: { + /** Factory for the generator + */ + create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator; + }; + + /** Defines parameters for credential generation */ + profiles: { + /** profiles supported by this generator; when `undefined`, + * the generator does not support the profile. + */ + [K in keyof typeof Profile]?: ProfileMetadata; + }; +}; diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts new file mode 100644 index 00000000000..57961a60033 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.spec.ts @@ -0,0 +1,102 @@ +import { mock } from "jest-mock-extended"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; + +import { PasswordRandomizer } from "../../engine"; +import { PassphrasePolicyConstraints } from "../../policies"; +import { PassphraseGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import effPassphrase from "./eff-word-list"; + +const dependencyProvider = mock(); + +describe("password - eff words generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(effPassphrase.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = effPassphrase.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: PassphraseGenerationOptions = { ...accountProfile.storage.initial }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a passphrase policy constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(PassphrasePolicyConstraints); + }); + + it("forwards the policy to the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + data: { + minNumberWords: 6, + capitalize: false, + includeNumber: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.numWords.min).toEqual(6); + }); + + it("combines multiple policies in the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + data: { + minNumberWords: 6, + capitalize: false, + includeNumber: false, + }, + }, + { + type: PolicyType.PasswordGenerator, + data: { + minNumberWords: 3, + capitalize: true, + includeNumber: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.numWords.min).toEqual(6); + expect(constraints.constraints.capitalize.requiredValue).toEqual(true); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/password/eff-word-list.ts b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts new file mode 100644 index 00000000000..fc86032bf6b --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/eff-word-list.ts @@ -0,0 +1,91 @@ +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; + +import { PasswordRandomizer } from "../../engine"; +import { passphraseLeastPrivilege, PassphrasePolicyConstraints } from "../../policies"; +import { + CredentialGenerator, + GeneratorDependencyProvider, + PassphraseGenerationOptions, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const passphrase: GeneratorMetadata = { + id: Algorithm.passphrase, + category: Type.password, + weight: 110, + i18nKeys: { + name: "passphrase", + credentialType: "passphrase", + generateCredential: "generatePassphrase", + credentialGenerated: "passphraseGenerated", + copyCredential: "copyPassphrase", + useCredential: "useThisPassphrase", + }, + capabilities: { + autogenerate: false, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "passphraseGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "numWords", + "wordSeparator", + "capitalize", + "includeNumber", + ]), + state: GENERATOR_DISK, + initial: { + numWords: 6, + wordSeparator: "-", + capitalize: false, + includeNumber: false, + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + constraints: { + type: PolicyType.PasswordGenerator, + default: { + wordSeparator: { maxLength: 1 }, + numWords: { + min: 3, + max: 20, + recommendation: 6, + }, + }, + create(policies, context) { + const initial = { + minNumberWords: 0, + capitalize: false, + includeNumber: false, + }; + const policy = policies.reduce(passphraseLeastPrivilege, initial); + const constraints = new PassphrasePolicyConstraints(policy, context.defaultConstraints); + return constraints; + }, + }, + }, + }, +}; + +export default passphrase; diff --git a/libs/tools/generator/core/src/metadata/password/random-password.spec.ts b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts new file mode 100644 index 00000000000..d91ceaac248 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/random-password.spec.ts @@ -0,0 +1,105 @@ +import { mock } from "jest-mock-extended"; + +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; + +import { PasswordRandomizer } from "../../engine"; +import { DynamicPasswordPolicyConstraints } from "../../policies"; +import { PasswordGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import password from "./random-password"; + +const dependencyProvider = mock(); + +describe("password - characters generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(password.engine.create(dependencyProvider)).toBeInstanceOf(PasswordRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = password.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: PasswordGenerationOptions = { ...accountProfile.storage.initial }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a passphrase policy constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(DynamicPasswordPolicyConstraints); + }); + + it("forwards the policy to the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + enabled: true, + data: { + minLength: 10, + capitalize: false, + useNumbers: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.length.min).toEqual(10); + }); + + it("combines multiple policies in the constraints", () => { + const context = { defaultConstraints: accountProfile.constraints.default }; + const policies = [ + { + type: PolicyType.PasswordGenerator, + enabled: true, + data: { + minLength: 14, + useSpecial: false, + useNumbers: false, + }, + }, + { + type: PolicyType.PasswordGenerator, + enabled: true, + data: { + minLength: 10, + useSpecial: true, + includeNumber: false, + }, + }, + ] as Policy[]; + + const constraints = accountProfile.constraints.create(policies, context); + + expect(constraints.constraints.length.min).toEqual(14); + expect(constraints.constraints.special.requiredValue).toEqual(true); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/password/random-password.ts b/libs/tools/generator/core/src/metadata/password/random-password.ts new file mode 100644 index 00000000000..693236b0967 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/password/random-password.ts @@ -0,0 +1,117 @@ +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { PasswordRandomizer } from "../../engine"; +import { DynamicPasswordPolicyConstraints, passwordLeastPrivilege } from "../../policies"; +import { + CredentialGenerator, + GeneratorDependencyProvider, + PasswordGeneratorSettings, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const password: GeneratorMetadata = deepFreeze({ + id: Algorithm.password, + category: Type.password, + weight: 100, + i18nKeys: { + name: "password", + generateCredential: "generatePassword", + credentialGenerated: "passwordGenerated", + credentialType: "password", + copyCredential: "copyPassword", + useCredential: "useThisPassword", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "passwordGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "length", + "ambiguous", + "uppercase", + "minUppercase", + "lowercase", + "minLowercase", + "number", + "minNumber", + "special", + "minSpecial", + ]), + state: GENERATOR_DISK, + initial: { + length: 14, + ambiguous: true, + uppercase: true, + minUppercase: 1, + lowercase: true, + minLowercase: 1, + number: true, + minNumber: 1, + special: false, + minSpecial: 0, + }, + options: { + deserializer(value) { + return value; + }, + clearOn: ["logout"], + }, + }, + constraints: { + type: PolicyType.PasswordGenerator, + default: { + length: { + min: 5, + max: 128, + recommendation: 14, + }, + minNumber: { + min: 0, + max: 9, + }, + minSpecial: { + min: 0, + max: 9, + }, + }, + create(policies, context) { + const initial = { + minLength: 0, + useUppercase: false, + useLowercase: false, + useNumbers: false, + numberCount: 0, + useSpecial: false, + specialCount: 0, + }; + const policy = policies.reduce(passwordLeastPrivilege, initial); + const constraints = new DynamicPasswordPolicyConstraints( + policy, + context.defaultConstraints, + ); + return constraints; + }, + }, + }, + }, +}); + +export default password; diff --git a/libs/tools/generator/core/src/metadata/profile-metadata.ts b/libs/tools/generator/core/src/metadata/profile-metadata.ts new file mode 100644 index 00000000000..4ac9139f632 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/profile-metadata.ts @@ -0,0 +1,80 @@ +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { SiteId } from "@bitwarden/common/tools/extension"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; +import { Constraints } from "@bitwarden/common/tools/types"; + +import { GeneratorConstraints } from "../types"; + +export type ProfileContext = { + /** The email address for the current user; + * `undefined` when no email is available. + */ + email?: string; + + /** Default application limits for the profile */ + defaultConstraints: Constraints; +}; + +type ProfileConstraints = { + /** The key used to locate this profile's policies in the admin console. + * When this type is undefined, no policy is defined for the profile. + */ + type?: PolicyType; + + /** default application limits for this profile; these are overridden + * by the policy + */ + default: Constraints; + + /** Constructs generator constraints from a policy. + * @param policies the administrative policy to apply to the provided constraints + * When `type` is undefined then `policy` is `undefined` this is an empty array. + * @param defaultConstraints application constraints; typically those defined in + * the `default` member, above. + * @returns the generator constraints to apply to this profile's options. + */ + create: (policies: Policy[], context: ProfileContext) => GeneratorConstraints; +}; + +/** Generator profiles partition generator operations + * according to where they're used within the password + * manager. Core profiles store their data using the + * generator's system storage. + */ +export type CoreProfileMetadata = { + /** distinguishes profile metadata types */ + type: "core"; + + /** plaintext import buffer */ + import?: ObjectKey, Options> & { format: "plain" }; + + /** persistent storage location */ + storage: ObjectKey; + + /** policy enforced when saving the options */ + constraints: ProfileConstraints; +}; + +/** Generator profiles partition generator operations + * according to where they're used within the password + * manager. Extension profiles store their data + * using the extension system. + */ +export type ExtensionProfileMetadata = { + /** distinguishes profile metadata types */ + type: "extension"; + + /** The extension site described by this metadata */ + site: Site; + + constraints: ProfileConstraints; +}; + +/** Generator profiles partition generator operations + * according to where they're used within the password + * manager + */ +export type ProfileMetadata = + | CoreProfileMetadata + | ExtensionProfileMetadata; diff --git a/libs/tools/generator/core/src/metadata/type.ts b/libs/tools/generator/core/src/metadata/type.ts new file mode 100644 index 00000000000..924b92883e5 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/type.ts @@ -0,0 +1,28 @@ +import { VendorId } from "@bitwarden/common/tools/extension"; + +import { AlgorithmsByType, Profile, Type } from "./data"; + +/** categorizes credentials according to their use-case outside of Bitwarden */ +export type CredentialType = keyof typeof Type; + +/** categorizes credentials according to their expected use-case within Bitwarden */ +export type GeneratorProfile = keyof typeof Profile; + +/** A type of password that may be generated by the credential generator. */ +export type PasswordAlgorithm = (typeof AlgorithmsByType.password)[number]; + +/** A type of username that may be generated by the credential generator. */ +export type UsernameAlgorithm = (typeof AlgorithmsByType.username)[number]; + +/** A type of email address that may be generated by the credential generator. */ +export type EmailAlgorithm = (typeof AlgorithmsByType.email)[number] | ForwarderExtensionId; + +/** Identifies a forwarding service */ +export type ForwarderExtensionId = { forwarder: VendorId }; + +/** A type of credential that can be generated by the credential generator. */ +// this is defined in terms of `AlgorithmsByType` to typecheck the keys of +// `AlgorithmsByType` against the keys of `CredentialType`. +export type CredentialAlgorithm = + | (typeof AlgorithmsByType)[CredentialType][number] + | ForwarderExtensionId; diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts new file mode 100644 index 00000000000..aba9680a448 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.spec.ts @@ -0,0 +1,58 @@ +import { mock } from "jest-mock-extended"; + +import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; + +import { UsernameRandomizer } from "../../engine"; +import { EffUsernameGenerationOptions, GeneratorDependencyProvider } from "../../types"; +import { Profile } from "../data"; +import { CoreProfileMetadata } from "../profile-metadata"; +import { isCoreProfile } from "../util"; + +import effWordList from "./eff-word-list"; + +const dependencyProvider = mock(); + +describe("username - eff words generator metadata", () => { + describe("engine.create", () => { + it("returns an email randomizer", () => { + expect(effWordList.engine.create(dependencyProvider)).toBeInstanceOf(UsernameRandomizer); + }); + }); + + describe("profiles[account]", () => { + let accountProfile: CoreProfileMetadata = null; + beforeEach(() => { + const profile = effWordList.profiles[Profile.account]; + if (isCoreProfile(profile)) { + accountProfile = profile; + } + }); + + describe("storage.options.deserializer", () => { + it("returns its input", () => { + const value: EffUsernameGenerationOptions = { + wordCapitalize: true, + wordIncludeNumber: true, + }; + + const result = accountProfile.storage.options.deserializer(value); + + expect(result).toBe(value); + }); + }); + + describe("constraints.create", () => { + // these tests check that the wiring is correct by exercising the behavior + // of functionality encapsulated by `create`. These methods may fail if the + // enclosed behaviors change. + + it("creates a effWordList constraints", () => { + const context = { defaultConstraints: {} }; + + const constraints = accountProfile.constraints.create([], context); + + expect(constraints).toBeInstanceOf(IdentityConstraint); + }); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/username/eff-word-list.ts b/libs/tools/generator/core/src/metadata/username/eff-word-list.ts new file mode 100644 index 00000000000..6373daf8ed5 --- /dev/null +++ b/libs/tools/generator/core/src/metadata/username/eff-word-list.ts @@ -0,0 +1,70 @@ +import { GENERATOR_DISK } from "@bitwarden/common/platform/state"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; +import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; +import { deepFreeze } from "@bitwarden/common/tools/util"; + +import { UsernameRandomizer } from "../../engine"; +import { + CredentialGenerator, + EffUsernameGenerationOptions, + GeneratorDependencyProvider, +} from "../../types"; +import { Algorithm, Profile, Type } from "../data"; +import { GeneratorMetadata } from "../generator-metadata"; + +const effWordList: GeneratorMetadata = deepFreeze({ + id: Algorithm.username, + category: Type.username, + weight: 400, + i18nKeys: { + name: "randomWord", + credentialType: "username", + generateCredential: "generateUsername", + credentialGenerated: "usernameGenerated", + copyCredential: "copyUsername", + useCredential: "useThisUsername", + }, + capabilities: { + autogenerate: true, + fields: [], + }, + engine: { + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new UsernameRandomizer(dependencies.randomizer); + }, + }, + profiles: { + [Profile.account]: { + type: "core", + storage: { + key: "effUsernameGeneratorSettings", + target: "object", + format: "plain", + classifier: new PublicClassifier([ + "wordCapitalize", + "wordIncludeNumber", + ]), + state: GENERATOR_DISK, + initial: { + wordCapitalize: false, + wordIncludeNumber: false, + website: null, + }, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + }, + constraints: { + default: {}, + create(_policies, _context) { + return new IdentityConstraint(); + }, + }, + }, + }, +}); + +export default effWordList; diff --git a/libs/tools/generator/core/src/metadata/util.spec.ts b/libs/tools/generator/core/src/metadata/util.spec.ts new file mode 100644 index 00000000000..2283699140b --- /dev/null +++ b/libs/tools/generator/core/src/metadata/util.spec.ts @@ -0,0 +1,218 @@ +import { VendorId } from "@bitwarden/common/tools/extension"; + +import { Algorithm, AlgorithmsByType } from "./data"; +import { ProfileMetadata } from "./profile-metadata"; +import { + isPasswordAlgorithm, + isUsernameAlgorithm, + isForwarderExtensionId, + isEmailAlgorithm, + isSameAlgorithm, + isCoreProfile, + isForwarderProfile, +} from "./util"; + +describe("credential generator metadata utility functions", () => { + describe("isPasswordAlgorithm", () => { + it("returns `true` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isPasswordAlgorithm(algorithm)).toBe(true); + } + }); + + it("returns `false` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isPasswordAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isPasswordAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is a forwarder extension", () => { + expect(isPasswordAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false); + }); + }); + + describe("isUsernameAlgorithm", () => { + it("returns `false` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isUsernameAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isUsernameAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isUsernameAlgorithm(algorithm)).toBe(true); + } + }); + + it("returns `false` when the algorithm is a forwarder extension", () => { + expect(isUsernameAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(false); + }); + }); + + describe("isForwarderExtensionId", () => { + it("returns `false` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isForwarderExtensionId(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isForwarderExtensionId(algorithm)).toBe(false); + } + }); + + it("returns `false` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isForwarderExtensionId(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is a forwarder extension", () => { + expect(isForwarderExtensionId({ forwarder: "bitwarden" as VendorId })).toBe(true); + }); + }); + + describe("isEmailAlgorithm", () => { + it("returns `false` when the algorithm is a password algorithm", () => { + for (const algorithm of AlgorithmsByType.password) { + expect(isEmailAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is an email algorithm", () => { + for (const algorithm of AlgorithmsByType.email) { + expect(isEmailAlgorithm(algorithm)).toBe(true); + } + }); + + it("returns `false` when the algorithm is a username algorithm", () => { + for (const algorithm of AlgorithmsByType.username) { + expect(isEmailAlgorithm(algorithm)).toBe(false); + } + }); + + it("returns `true` when the algorithm is a forwarder extension", () => { + expect(isEmailAlgorithm({ forwarder: "bitwarden" as VendorId })).toBe(true); + }); + }); + + describe("isSameAlgorithm", () => { + it("returns `true` when the algorithms are equal", () => { + // identical object + expect(isSameAlgorithm(Algorithm.catchall, Algorithm.catchall)).toBe(true); + + // equal object + expect(isSameAlgorithm(Algorithm.catchall, `${Algorithm.catchall}`)).toBe(true); + }); + + it("returns `false` when the algorithms are different", () => { + // not an exhaustive list + expect(isSameAlgorithm(Algorithm.catchall, Algorithm.passphrase)).toBe(false); + expect(isSameAlgorithm(Algorithm.passphrase, Algorithm.password)).toBe(false); + expect(isSameAlgorithm(Algorithm.password, Algorithm.plusAddress)).toBe(false); + expect(isSameAlgorithm(Algorithm.plusAddress, Algorithm.username)).toBe(false); + expect(isSameAlgorithm(Algorithm.username, Algorithm.passphrase)).toBe(false); + }); + + it("returns `true` when the algorithms refer to a forwarder with a matching vendor", () => { + const someVendor = { forwarder: "bitwarden" as VendorId }; + const sameVendor = { forwarder: "bitwarden" as VendorId }; + expect(isSameAlgorithm(someVendor, sameVendor)).toBe(true); + }); + + it("returns `false` when the algorithms refer to a forwarder with a different vendor", () => { + const someVendor = { forwarder: "bitwarden" as VendorId }; + const sameVendor = { forwarder: "bytewarden" as VendorId }; + expect(isSameAlgorithm(someVendor, sameVendor)).toBe(false); + }); + + it("returns `false` when the algorithms refer to a forwarder and a core algorithm", () => { + const someVendor = { forwarder: "bitwarden" as VendorId }; + // not an exhaustive list + expect(isSameAlgorithm(someVendor, Algorithm.plusAddress)).toBe(false); + expect(isSameAlgorithm(Algorithm.username, someVendor)).toBe(false); + }); + }); + + describe("isCoreProfile", () => { + it("returns `true` when the profile's type is `core`", () => { + const profile: ProfileMetadata = { + type: "core", + storage: null, + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isCoreProfile(profile)).toBe(true); + }); + + it("returns `false` when the profile's type is `extension`", () => { + const profile: ProfileMetadata = { + type: "extension", + site: "forwarder", + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isCoreProfile(profile)).toBe(false); + }); + }); + + describe("isForwarderProfile", () => { + it("returns `false` when the profile's type is `core`", () => { + const profile: ProfileMetadata = { + type: "core", + storage: null, + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isForwarderProfile(profile)).toBe(false); + }); + + it("returns `true` when the profile's type is `extension` and the site is `forwarder`", () => { + const profile: ProfileMetadata = { + type: "extension", + site: "forwarder", + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isForwarderProfile(profile)).toBe(true); + }); + + it("returns `false` when the profile's type is `extension` and the site is not `forwarder`", () => { + const profile: ProfileMetadata = { + type: "extension", + site: "not-a-forwarder" as any, + constraints: { + default: {}, + create: () => null, + }, + }; + + expect(isForwarderProfile(profile)).toBe(false); + }); + }); +}); diff --git a/libs/tools/generator/core/src/metadata/util.ts b/libs/tools/generator/core/src/metadata/util.ts new file mode 100644 index 00000000000..e85061720ad --- /dev/null +++ b/libs/tools/generator/core/src/metadata/util.ts @@ -0,0 +1,60 @@ +import { AlgorithmsByType } from "./data"; +import { CoreProfileMetadata, ExtensionProfileMetadata, ProfileMetadata } from "./profile-metadata"; +import { + CredentialAlgorithm, + EmailAlgorithm, + ForwarderExtensionId, + PasswordAlgorithm, + UsernameAlgorithm, +} from "./type"; + +/** Returns true when the input algorithm is a password algorithm. */ +export function isPasswordAlgorithm( + algorithm: CredentialAlgorithm, +): algorithm is PasswordAlgorithm { + return AlgorithmsByType.password.includes(algorithm as any); +} + +/** Returns true when the input algorithm is a username algorithm. */ +export function isUsernameAlgorithm( + algorithm: CredentialAlgorithm, +): algorithm is UsernameAlgorithm { + return AlgorithmsByType.username.includes(algorithm as any); +} + +/** Returns true when the input algorithm is a forwarder integration. */ +export function isForwarderExtensionId( + algorithm: CredentialAlgorithm, +): algorithm is ForwarderExtensionId { + return algorithm && typeof algorithm === "object" && "forwarder" in algorithm; +} + +/** Returns true when the input algorithm is an email algorithm. */ +export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm { + return AlgorithmsByType.email.includes(algorithm as any) || isForwarderExtensionId(algorithm); +} + +/** Returns true when the algorithms are the same. */ +export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) { + if (lhs === rhs) { + return true; + } else if (isForwarderExtensionId(lhs) && isForwarderExtensionId(rhs)) { + return lhs.forwarder === rhs.forwarder; + } else { + return false; + } +} + +/** Returns true when the input describes a core profile. */ +export function isCoreProfile( + value: ProfileMetadata, +): value is CoreProfileMetadata { + return value.type === "core"; +} + +/** Returns true when the input describes a forwarder extension profile. */ +export function isForwarderProfile( + value: ProfileMetadata, +): value is ExtensionProfileMetadata { + return value.type === "extension" && value.site === "forwarder"; +} diff --git a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts index f61db0b33ec..f37a8b21a3f 100644 --- a/libs/tools/generator/core/src/policies/available-algorithms-policy.ts +++ b/libs/tools/generator/core/src/policies/available-algorithms-policy.ts @@ -4,12 +4,8 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { - CredentialAlgorithm, - EmailAlgorithms, - PasswordAlgorithms, - UsernameAlgorithms, -} from "@bitwarden/generator-core"; + +import { CredentialAlgorithm, EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from ".."; /** Reduces policies to a set of available algorithms * @param policies the policies to reduce diff --git a/libs/tools/generator/core/src/policies/catchall-constraints.ts b/libs/tools/generator/core/src/policies/catchall-constraints.ts index 47476a304a9..7793180988d 100644 --- a/libs/tools/generator/core/src/policies/catchall-constraints.ts +++ b/libs/tools/generator/core/src/policies/catchall-constraints.ts @@ -24,7 +24,7 @@ export class CatchallConstraints implements StateConstraints> = {}; diff --git a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts index 160da706937..ff6f9b8fd50 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts @@ -74,6 +74,7 @@ const SomeNameKey = "passphraseKey"; const SomeGenerateKey = "generateKey"; const SomeGeneratedValueKey = "generatedValueKey"; const SomeCopyKey = "copyKey"; +const SomeUseGeneratedValueKey = "useGeneratedValueKey"; // fake the configuration const SomeConfiguration: CredentialGeneratorConfiguration = { @@ -83,6 +84,7 @@ const SomeConfiguration: CredentialGeneratorConfiguration { let fixture: ComponentFixture; let sendListFiltersService: SendListFiltersService; let billingAccountProfileStateService: MockProxy; + let accountService: MockProxy; + const userId = "userId" as UserId; beforeEach(async () => { sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); sendListFiltersService.resetFilterForm = jest.fn(); billingAccountProfileStateService = mock(); + accountService = mock(); - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + accountService.activeAccount$ = of({ + id: userId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); await TestBed.configureTestingModule({ imports: [ @@ -37,10 +48,8 @@ describe("SendListFiltersComponent", () => { providers: [ { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: SendListFiltersService, useValue: sendListFiltersService }, - { - provide: BillingAccountProfileStateService, - useValue: billingAccountProfileStateService, - }, + { provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService }, + { provide: AccountService, useValue: accountService }, ], }).compileComponents(); @@ -57,6 +66,7 @@ describe("SendListFiltersComponent", () => { let canAccessPremium: boolean | undefined; component["canAccessPremium$"].subscribe((value) => (canAccessPremium = value)); expect(canAccessPremium).toBe(true); + expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(userId); }); it("should call resetFilterForm on ngOnDestroy", () => { diff --git a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts index b313ced742a..d42eab382e9 100644 --- a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts +++ b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy } from "@angular/core"; import { ReactiveFormsModule } from "@angular/forms"; -import { Observable } from "rxjs"; +import { Observable, of, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ChipSelectComponent } from "@bitwarden/components"; import { SendListFiltersService } from "../services/send-list-filters.service"; @@ -23,8 +24,15 @@ export class SendListFiltersComponent implements OnDestroy { constructor( private sendListFiltersService: SendListFiltersService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.canAccessPremium$ = accountService.activeAccount$.pipe( + switchMap((account) => + account + ? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), + ); } ngOnDestroy(): void { diff --git a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html index a0f6c09f83e..d244be15087 100644 --- a/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html +++ b/libs/tools/send/send-ui/src/send-list-items-container/send-list-items-container.component.html @@ -14,17 +14,16 @@ [queryParams]="{ sendId: send.id, type: send.type }" appStopClick type="button" - class="tw-pb-1" > {{ send.name }} diff --git a/libs/tools/send/send-ui/tsconfig.json b/libs/tools/send/send-ui/tsconfig.json index c52bfd7b0df..e6d6680ad40 100644 --- a/libs/tools/send/send-ui/tsconfig.json +++ b/libs/tools/send/send-ui/tsconfig.json @@ -1,5 +1,22 @@ { - "extends": "../../../shared/tsconfig.libs", + "extends": "../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../admin-console/src/common"], + "@bitwarden/angular/*": ["../../../angular/src/*"], + "@bitwarden/auth/common": ["../../../auth/src/common"], + "@bitwarden/common/*": ["../../../common/src/*"], + "@bitwarden/components": ["../../../components/src"], + "@bitwarden/generator-components": ["../../../tools/generator/components/src"], + "@bitwarden/generator-core": ["../../../tools/generator/core/src"], + "@bitwarden/generator-history": ["../../../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../../../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../../../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../../../key-management/src"], + "@bitwarden/platform": ["../../../platform/src"], + "@bitwarden/ui-common": ["../../../ui/common/src"] + } + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/ui/README.md b/libs/ui/README.md new file mode 100644 index 00000000000..e245aac71cb --- /dev/null +++ b/libs/ui/README.md @@ -0,0 +1,5 @@ +# UI Foundation + +Core UI libraries maintained by the UI Foundation team. + +- _ui-common_: Low-level utilities for Angular applications. diff --git a/libs/ui/common/package.json b/libs/ui/common/package.json new file mode 100644 index 00000000000..f1b03a3ebd0 --- /dev/null +++ b/libs/ui/common/package.json @@ -0,0 +1,15 @@ +{ + "name": "@bitwarden/ui-common", + "version": "0.0.0", + "description": "Low-level utilities for Angular applications", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0" +} diff --git a/libs/ui/common/src/di/index.ts b/libs/ui/common/src/di/index.ts new file mode 100644 index 00000000000..7b0705f1364 --- /dev/null +++ b/libs/ui/common/src/di/index.ts @@ -0,0 +1,2 @@ +export * from "./safe-injection-token"; +export * from "./safe-provider"; diff --git a/libs/ui/common/src/di/safe-injection-token.ts b/libs/ui/common/src/di/safe-injection-token.ts new file mode 100644 index 00000000000..aad081e1c76 --- /dev/null +++ b/libs/ui/common/src/di/safe-injection-token.ts @@ -0,0 +1,14 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { InjectionToken } from "@angular/core"; + +declare const tag: unique symbol; +/** + * A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter. + * @remarks The default angular implementation does not use the generic type to define the structure of the object, + * so the structural type system will not complain about a mismatch in the type parameter. + * This is solved by assigning T to an arbitrary private property. + */ +export class SafeInjectionToken extends InjectionToken { + private readonly [tag]: T; +} diff --git a/libs/ui/common/src/di/safe-provider.ts b/libs/ui/common/src/di/safe-provider.ts new file mode 100644 index 00000000000..002aa69a500 --- /dev/null +++ b/libs/ui/common/src/di/safe-provider.ts @@ -0,0 +1,138 @@ +import { Provider } from "@angular/core"; +import { Constructor, Opaque } from "type-fest"; + +import { SafeInjectionToken } from "./safe-injection-token"; + +/** + * The return type of the {@link safeProvider} helper function. + * Used to distinguish a type safe provider definition from a non-type safe provider definition. + */ +export type SafeProvider = Opaque; + +// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0 +type AbstractConstructor = abstract new (...args: any) => T; + +type MapParametersToDeps = { + [K in keyof T]: AbstractConstructor | SafeInjectionToken; +}; + +type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; + +/** + * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken + */ +type ProviderInstanceType = + T extends SafeInjectionToken + ? InstanceType> + : T extends Constructor | AbstractConstructor + ? InstanceType + : never; + +/** + * Represents a dependency provided with the useClass option. + */ +type SafeClassProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends Constructor>, + D extends MapParametersToDeps>, +> = { + provide: A; + useClass: I; + deps: D; +}; + +/** + * Represents a dependency provided with the useValue option. + */ +type SafeValueProvider, V extends SafeInjectionTokenType> = { + provide: A; + useValue: V; +}; + +/** + * Represents a dependency provided with the useFactory option. + */ +type SafeFactoryProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends (...args: any) => ProviderInstanceType, + D extends MapParametersToDeps>, +> = { + provide: A; + useFactory: I; + deps: D; + multi?: boolean; +}; + +/** + * Represents a dependency provided with the useExisting option. + */ +type SafeExistingProvider< + A extends Constructor | AbstractConstructor | SafeInjectionToken, + I extends Constructor> | AbstractConstructor>, +> = { + provide: A; + useExisting: I; +}; + +/** + * Represents a dependency where there is no abstract token, the token is the implementation + */ +type SafeConcreteProvider< + I extends Constructor, + D extends MapParametersToDeps>, +> = { + provide: I; + deps: D; +}; + +/** + * If useAngularDecorators: true is specified, do not require a deps array. + * This is a manual override for where @Injectable decorators are used + */ +type UseAngularDecorators = Omit & { + useAngularDecorators: true; +}; + +/** + * Represents a type with a deps array that may optionally be overridden with useAngularDecorators + */ +type AllowAngularDecorators = T | UseAngularDecorators; + +/** + * A factory function that creates a provider for the ngModule providers array. + * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. + * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, + * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. + * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) + * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) + * @returns The exact same object without modification (pass-through). + */ +export const safeProvider = < + // types for useClass + AClass extends AbstractConstructor | SafeInjectionToken, + IClass extends Constructor>, + DClass extends MapParametersToDeps>, + // types for useValue + AValue extends SafeInjectionToken, + VValue extends SafeInjectionTokenType, + // types for useFactory + AFactory extends AbstractConstructor | SafeInjectionToken, + IFactory extends (...args: any) => ProviderInstanceType, + DFactory extends MapParametersToDeps>, + // types for useExisting + AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, + IExisting extends + | Constructor> + | AbstractConstructor>, + // types for no token + IConcrete extends Constructor, + DConcrete extends MapParametersToDeps>, +>( + provider: + | AllowAngularDecorators> + | SafeValueProvider + | AllowAngularDecorators> + | SafeExistingProvider + | AllowAngularDecorators> + | Constructor, +): SafeProvider => provider as SafeProvider; diff --git a/libs/angular/src/platform/utils/safe-provider.type.spec.ts b/libs/ui/common/src/di/safe-provider.type.spec.ts similarity index 96% rename from libs/angular/src/platform/utils/safe-provider.type.spec.ts rename to libs/ui/common/src/di/safe-provider.type.spec.ts index 6fe6d0d0b6c..afc7071af1e 100644 --- a/libs/angular/src/platform/utils/safe-provider.type.spec.ts +++ b/libs/ui/common/src/di/safe-provider.type.spec.ts @@ -11,7 +11,7 @@ class FooFactory { } abstract class FooService { - createFoo: (str: string) => string; + abstract createFoo(str: string): string; } class DefaultFooService implements FooService { @@ -29,7 +29,7 @@ class BarFactory { } abstract class BarService { - createBar: (num: number) => number; + abstract createBar(num: number): number; } class DefaultBarService implements BarService { diff --git a/libs/components/src/shared/i18n.pipe.ts b/libs/ui/common/src/i18n.pipe.ts similarity index 77% rename from libs/components/src/shared/i18n.pipe.ts rename to libs/ui/common/src/i18n.pipe.ts index 91bf0b3198d..fdcfec0ceac 100644 --- a/libs/components/src/shared/i18n.pipe.ts +++ b/libs/ui/common/src/i18n.pipe.ts @@ -3,7 +3,13 @@ import { Pipe, PipeTransform } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; /** - * Temporarily duplicate this pipe + * Localizes the specified string. + * + * @example + * {{ 'key' | i18n }} + * + * @example + * {{ 'key' | i18n: 'param1' }} */ @Pipe({ name: "i18n", diff --git a/libs/ui/common/src/index.ts b/libs/ui/common/src/index.ts new file mode 100644 index 00000000000..97e55108116 --- /dev/null +++ b/libs/ui/common/src/index.ts @@ -0,0 +1,2 @@ +export * from "./di"; +export * from "./i18n.pipe"; diff --git a/libs/ui/common/tsconfig.json b/libs/ui/common/tsconfig.json new file mode 100644 index 00000000000..31062d41a1c --- /dev/null +++ b/libs/ui/common/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/common/*": ["../../common/src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/vault/jest.config.js b/libs/vault/jest.config.js index e960eed9f1a..e33c115e8dd 100644 --- a/libs/vault/jest.config.js +++ b/libs/vault/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts b/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts index d6d95565869..72bbb0da12c 100644 --- a/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts +++ b/libs/vault/src/cipher-form/abstractions/totp-capture.service.ts @@ -6,4 +6,9 @@ export abstract class TotpCaptureService { * Captures a TOTP secret and returns it as a string. Returns null if no TOTP secret was found. */ abstract captureTotpSecret(): Promise; + /** + * Returns whether the TOTP secret can be captured from the current tab. + * Only available in the browser extension and when not in a popout window. + */ + abstract canCaptureTotp(window: Window): boolean; } diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 98ccdec360e..f7146776b73 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { importProvidersFrom } from "@angular/core"; import { action } from "@storybook/addon-actions"; import { @@ -33,7 +31,8 @@ import { PasswordRepromptService, } from "@bitwarden/vault"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests"; import { CipherFormService } from "./abstractions/cipher-form.service"; @@ -154,6 +153,7 @@ export default { provide: TotpCaptureService, useValue: { captureTotpSecret: () => Promise.resolve("some-value"), + canCaptureTotp: () => true, }, }, { @@ -234,7 +234,7 @@ export const Edit: Story = { config: { ...defaultConfig, mode: "edit", - originalCipher: defaultConfig.originalCipher, + originalCipher: defaultConfig.originalCipher!, }, }, }; @@ -245,7 +245,7 @@ export const PartialEdit: Story = { config: { ...defaultConfig, mode: "partial-edit", - originalCipher: defaultConfig.originalCipher, + originalCipher: defaultConfig.originalCipher!, }, }, }; @@ -256,7 +256,7 @@ export const Clone: Story = { config: { ...defaultConfig, mode: "clone", - originalCipher: defaultConfig.originalCipher, + originalCipher: defaultConfig.originalCipher!, }, }, }; @@ -269,7 +269,7 @@ export const NoPersonalOwnership: Story = { mode: "add", allowPersonalOwnership: false, originalCipher: defaultConfig.originalCipher, - organizations: defaultConfig.organizations, + organizations: defaultConfig.organizations!, }, }, }; diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts index 749093902d7..8e0d4f7a665 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts @@ -98,7 +98,7 @@ describe("DeleteAttachmentComponent", () => { expect(showToast).toHaveBeenCalledWith({ variant: "success", - title: null, + title: "", message: "deletedAttachment", }); }); diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts index d5a7039ad70..b1ada907b1d 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; @@ -24,10 +22,10 @@ import { }) export class DeleteAttachmentComponent { /** Id of the cipher associated with the attachment */ - @Input({ required: true }) cipherId: string; + @Input({ required: true }) cipherId!: string; /** The attachment that is can be deleted */ - @Input({ required: true }) attachment: AttachmentView; + @Input({ required: true }) attachment!: AttachmentView; /** Emits when the attachment is successfully deleted */ @Output() onDeletionSuccess = new EventEmitter(); @@ -56,7 +54,7 @@ export class DeleteAttachmentComponent { this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("deletedAttachment"), }); diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html index 445908679c3..16bccebb939 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html @@ -2,9 +2,11 @@ *ngIf="type === 'password'" [disableMargin]="disableMargin" (onGenerated)="onCredentialGenerated($event)" + (onAlgorithm)="algorithm($event)" > diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index 1a2bac2ce50..fdde5e15d91 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -5,7 +5,7 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { GeneratorModule } from "@bitwarden/generator-components"; -import { GeneratedCredential } from "@bitwarden/generator-core"; +import { AlgorithmInfo, GeneratedCredential } from "@bitwarden/generator-core"; /** * Renders a password or username generator UI and emits the most recently generated value. @@ -18,6 +18,9 @@ import { GeneratedCredential } from "@bitwarden/generator-core"; imports: [CommonModule, GeneratorModule], }) export class CipherFormGeneratorComponent { + @Input() + algorithm: (selected: AlgorithmInfo) => void; + /** * The type of generator form to show. */ diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts index f63a974d487..62d171b8436 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts @@ -38,6 +38,8 @@ describe("AddEditCustomFieldDialogComponent", () => { fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent); component = fixture.componentInstance; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions fixture.detectChanges; }); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html index 63941c6a467..fab3c8f1ab1 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.html @@ -46,7 +46,7 @@ bitSuffix bitPasswordInputToggle data-testid="visibility-for-custom-hidden-field" - [disabled]="!canViewPasswords(i)" + *ngIf="canViewPasswords(i)" (toggledChange)="logHiddenEvent($event)" > diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts index 0c0fa1b4184..b523d9ef933 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -19,6 +19,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { DialogService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "../../../../../components/src/form-field/password-input-toggle.directive"; import { CipherFormConfig } from "../../abstractions/cipher-form-config.service"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -111,7 +113,7 @@ describe("CustomFieldsComponent", () => { ]); }); - it("forbids a user to view hidden fields when the cipher `viewPassword` is false", () => { + it("when `viewPassword` is false the user cannot see the view toggle option", () => { originalCipherView.viewPassword = false; originalCipherView.fields = mockFieldViews; @@ -121,7 +123,20 @@ describe("CustomFieldsComponent", () => { const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); - expect(button.nativeElement.disabled).toBe(true); + expect(button).toBeFalsy(); + }); + + it("when `viewPassword` is true the user can see the view toggle option", () => { + originalCipherView.viewPassword = true; + originalCipherView.fields = mockFieldViews; + + component.ngOnInit(); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective)); + + expect(button).toBeTruthy(); }); describe("linkedFieldOptions", () => { diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index d723bad5751..cd75fe8fba1 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -167,6 +167,10 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { ); }); + if (!this.cipherFormContainer.originalCipherView?.viewPassword) { + this.customFieldsForm.disable(); + } + // Disable the form if in partial-edit mode // Must happen after the initial fields are populated if (this.cipherFormContainer.config.mode === "partial-edit") { diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index 232a4b2d27b..69cfd419742 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -14,6 +14,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive"; import { CipherFormGenerationService } from "../../abstractions/cipher-form-generation.service"; @@ -434,6 +436,7 @@ describe("LoginDetailsSectionComponent", () => { }); it("should call captureTotp when the capture totp button is clicked", fakeAsync(() => { + jest.spyOn(component, "canCaptureTotp", "get").mockReturnValue(true); component.captureTotp = jest.fn(); fixture.detectChanges(); @@ -445,7 +448,8 @@ describe("LoginDetailsSectionComponent", () => { })); describe("canCaptureTotp", () => { - it("should return true when totpCaptureService is present and totp is editable", () => { + it("should return true when totpCaptureService is present and totpCaptureService.canCaptureTotp is true and totp is editable", () => { + jest.spyOn(component, "canCaptureTotp", "get").mockReturnValue(true); component.loginDetailsForm.controls.totp.enable(); expect(component.canCaptureTotp).toBe(true); }); diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts index 0a6772e5ef1..2296932aca3 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.ts @@ -65,10 +65,14 @@ export class LoginDetailsSectionComponent implements OnInit { newPasswordGenerated: boolean; /** - * Whether the TOTP field can be captured from the current tab. Only available in the browser extension. + * Whether the TOTP field can be captured from the current tab. Only available in the browser extension and + * when not in a popout window. */ get canCaptureTotp() { - return this.totpCaptureService != null && this.loginDetailsForm.controls.totp.enabled; + return ( + !!this.totpCaptureService?.canCaptureTotp(window) && + this.loginDetailsForm.controls.totp.enabled + ); } private datePipe = inject(DatePipe); diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index 914654c522d..134897c9356 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -2,11 +2,15 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { CardComponent, FormFieldModule, @@ -16,6 +20,7 @@ import { SelectModule, TypographyModule, } from "@bitwarden/components"; +import { generate_ssh_key } from "@bitwarden/sdk-internal"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -50,20 +55,35 @@ export class SshKeySectionComponent implements OnInit { * leaving as just null gets inferred as `unknown` */ sshKeyForm = this.formBuilder.group({ - privateKey: null as string | null, - publicKey: null as string | null, - keyFingerprint: null as string | null, + privateKey: [""], + publicKey: [""], + keyFingerprint: [""], }); constructor( private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, private i18nService: I18nService, - ) {} + private sdkService: SdkService, + ) { + this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm); + this.sshKeyForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + const data = new SshKeyView(); + data.privateKey = value.privateKey; + data.publicKey = value.publicKey; + data.keyFingerprint = value.keyFingerprint; + this.cipherFormContainer.patchCipher((cipher) => { + cipher.sshKey = data; + return cipher; + }); + }); + } - ngOnInit() { - if (this.originalCipherView?.card) { + async ngOnInit() { + if (this.originalCipherView?.sshKey) { this.setInitialValues(); + } else { + await this.generateSshKey(); } this.sshKeyForm.disable(); @@ -79,4 +99,14 @@ export class SshKeySectionComponent implements OnInit { keyFingerprint, }); } + + private async generateSshKey() { + await firstValueFrom(this.sdkService.client$); + const sshKey = generate_ssh_key("Ed25519"); + this.sshKeyForm.setValue({ + privateKey: sshKey.private_key, + publicKey: sshKey.public_key, + keyFingerprint: sshKey.key_fingerprint, + }); + } } diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index c2fbd024aa8..93a53345d3a 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -7,6 +7,7 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -31,12 +32,17 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { private cipherService: CipherService = inject(CipherService); private folderService: FolderService = inject(FolderService); private collectionService: CollectionService = inject(CollectionService); + private accountService = inject(AccountService); + + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); async buildConfig( mode: CipherFormMode, cipherId?: CipherId, cipherType?: CipherType, ): Promise { + const activeUserId = await firstValueFrom(this.activeUserId$); + const [organizations, collections, allowPersonalOwnership, folders, cipher] = await firstValueFrom( combineLatest([ @@ -49,9 +55,9 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { ), ), this.allowPersonalOwnership$, - this.folderService.folders$.pipe( + this.folderService.folders$(activeUserId).pipe( switchMap((f) => - this.folderService.folderViews$.pipe( + this.folderService.folderViews$(activeUserId).pipe( filter((d) => d.length - 1 === f.length), // -1 for "No Folder" in folderViews$ ), ), diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts index acfdfa3337d..0c2ca35cbbc 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts @@ -6,6 +6,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NEVER, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { StateProvider } from "@bitwarden/common/platform/state"; import { OrganizationId } from "@bitwarden/common/types/guid"; @@ -47,16 +48,22 @@ export class AttachmentsV2ViewComponent { private keyService: KeyService, private billingAccountProfileStateService: BillingAccountProfileStateService, private stateProvider: StateProvider, + private accountService: AccountService, ) { this.subscribeToHasPremiumCheck(); this.subscribeToOrgKey(); } subscribeToHasPremiumCheck() { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntilDestroyed()) - .subscribe((data) => { - this.canAccessPremium = data; + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntilDestroyed(), + ) + .subscribe((hasPremium) => { + this.canAccessPremium = hasPremium; }); } diff --git a/libs/vault/src/cipher-view/card-details/card-details-view.component.html b/libs/vault/src/cipher-view/card-details/card-details-view.component.html index d805408b385..fff771b6465 100644 --- a/libs/vault/src/cipher-view/card-details/card-details-view.component.html +++ b/libs/vault/src/cipher-view/card-details/card-details-view.component.html @@ -22,7 +22,7 @@ readonly bitInput type="password" - [value]="card.number" + [value]="card.number | creditCardNumber: cipher.card.brand" aria-readonly="true" data-testid="cardholder-number" class="tw-font-mono" diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 57af96258fa..f872ad0cf15 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -1,11 +1,12 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; -import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; import { CollectionId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -48,6 +49,8 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide export class CipherViewComponent implements OnChanges, OnDestroy { @Input({ required: true }) cipher: CipherView | null = null; + private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); + /** * Optional list of collections the cipher is assigned to. If none are provided, they will be fetched using the * `CipherService` and the `collectionIds` property of the cipher. @@ -66,6 +69,7 @@ export class CipherViewComponent implements OnChanges, OnDestroy { private organizationService: OrganizationService, private collectionService: CollectionService, private folderService: FolderService, + private accountService: AccountService, ) {} async ngOnChanges() { @@ -97,8 +101,8 @@ export class CipherViewComponent implements OnChanges, OnDestroy { return false; } - const { username, password, totp } = this.cipher.login; - return username || password || totp; + const { username, password, totp, fido2Credentials } = this.cipher.login; + return username || password || totp || fido2Credentials; } get hasAutofill() { @@ -136,8 +140,14 @@ export class CipherViewComponent implements OnChanges, OnDestroy { } if (this.cipher.folderId) { + const activeUserId = await firstValueFrom(this.activeUserId$); + + if (!activeUserId) { + return; + } + this.folder$ = this.folderService - .getDecrypted$(this.cipher.folderId) + .getDecrypted$(this.cipher.folderId, activeUserId) .pipe(takeUntil(this.destroyed$)); } } diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html index 45ddc3c1dea..8eee79729af 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.html @@ -36,17 +36,21 @@ diff --git a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts index b41b351e197..313607ce3a6 100644 --- a/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts +++ b/libs/vault/src/cipher-view/custom-fields/custom-fields-v2.component.ts @@ -59,6 +59,10 @@ export class CustomFieldV2Component implements OnInit { return this.i18nService.t(linkedType.i18nKey); } + get canViewPassword() { + return this.cipher.viewPassword; + } + async logHiddenEvent(hiddenFieldVisible: boolean) { if (hiddenFieldVisible) { await this.eventCollectionService.collect( diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 8503604bf7c..b4a0d4841f8 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -116,7 +116,7 @@ {{ "verificationCodeTotp" | i18n }}
    diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts index c8ac0598c97..39fcc39bf58 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts @@ -4,19 +4,24 @@ import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { CopyClickDirective } from "@bitwarden/angular/directives/copy-click.directive"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { BitFormFieldComponent, ToastService } from "@bitwarden/components"; +import { CopyClickDirective, BitFormFieldComponent, ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive"; import { LoginCredentialsViewComponent } from "./login-credentials-view.component"; @@ -26,6 +31,17 @@ describe("LoginCredentialsViewComponent", () => { let fixture: ComponentFixture; const hasPremiumFromAnySource$ = new BehaviorSubject(true); + const mockAccount = { + id: "test-user-id" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + type: 0, + status: 0, + kdf: 0, + kdfIterations: 0, + }; + const activeAccount$ = new BehaviorSubject(mockAccount); const cipher = { id: "cipher-id", @@ -48,8 +64,11 @@ describe("LoginCredentialsViewComponent", () => { providers: [ { provide: BillingAccountProfileStateService, - useValue: mock({ hasPremiumFromAnySource$ }), + useValue: mock({ + hasPremiumFromAnySource$: () => hasPremiumFromAnySource$, + }), }, + { provide: AccountService, useValue: mock({ activeAccount$ }) }, { provide: PremiumUpgradePromptService, useValue: mock() }, { provide: EventCollectionService, useValue: mock({ collect }) }, { provide: PlatformUtilsService, useValue: mock() }, diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts index 0c42c2ddda7..281e187f78b 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -2,24 +2,35 @@ // @ts-strict-ignore import { CommonModule, DatePipe } from "@angular/common"; import { Component, inject, Input } from "@angular/core"; -import { Observable, shareReplay } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + filter, + map, + Observable, + shareReplay, + switchMap, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { + BadgeModule, + ColorPasswordModule, FormFieldModule, + IconButtonModule, SectionComponent, SectionHeaderComponent, TypographyModule, - IconButtonModule, - BadgeModule, - ColorPasswordModule, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PremiumUpgradePromptService } from "../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component"; import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; @@ -48,12 +59,31 @@ type TotpCodeValues = { ], }) export class LoginCredentialsViewComponent { - @Input() cipher: CipherView; + @Input() + get cipher(): CipherView { + return this._cipher$.value; + } + set cipher(value: CipherView) { + this._cipher$.next(value); + } + private _cipher$ = new BehaviorSubject(null); - isPremium$: Observable = - this.billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( - shareReplay({ refCount: true, bufferSize: 1 }), - ); + private _userHasPremium$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); + + allowTotpGeneration$: Observable = combineLatest([ + this._userHasPremium$, + this._cipher$.pipe(filter((c) => c != null)), + ]).pipe( + map(([userHasPremium, cipher]) => { + // User premium status only applies to personal ciphers, organizationUseTotp applies to organization ciphers + return (userHasPremium && cipher.organizationId == null) || cipher.organizationUseTotp; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); showPasswordCount: boolean = false; passwordRevealed: boolean = false; totpCodeCopyObj: TotpCodeValues; @@ -64,6 +94,7 @@ export class LoginCredentialsViewComponent { private i18nService: I18nService, private premiumUpgradeService: PremiumUpgradePromptService, private eventCollectionService: EventCollectionService, + private accountService: AccountService, ) {} get fido2CredentialCreationDateValue(): string { diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts index 4c84b2852a8..21f82234b2c 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts @@ -8,6 +8,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; import { SectionHeaderComponent } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitInputDirective } from "../../../../components/src/input/input.directive"; import { ViewIdentitySectionsComponent } from "./view-identity-sections.component"; diff --git a/libs/vault/src/components/copy-cipher-field.directive.ts b/libs/vault/src/components/copy-cipher-field.directive.ts index 19bf25a2e2e..1eb96a30449 100644 --- a/libs/vault/src/components/copy-cipher-field.directive.ts +++ b/libs/vault/src/components/copy-cipher-field.directive.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Directive, HostBinding, HostListener, Input, OnChanges, Optional } from "@angular/core"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -28,9 +26,9 @@ export class CopyCipherFieldDirective implements OnChanges { alias: "appCopyField", required: true, }) - action: Exclude; + action!: Exclude; - @Input({ required: true }) cipher: CipherView; + @Input({ required: true }) cipher!: CipherView; constructor( private copyCipherFieldService: CopyCipherFieldService, @@ -52,7 +50,7 @@ export class CopyCipherFieldDirective implements OnChanges { @HostListener("click") async copy() { const value = this.getValueToCopy(); - await this.copyCipherFieldService.copy(value, this.action, this.cipher); + await this.copyCipherFieldService.copy(value ?? "", this.action, this.cipher); } async ngOnChanges() { @@ -69,7 +67,7 @@ export class CopyCipherFieldDirective implements OnChanges { // If the directive is used on a menu item, update the menu item to prevent keyboard navigation if (this.menuItemDirective) { - this.menuItemDirective.disabled = this.disabled; + this.menuItemDirective.disabled = this.disabled ?? false; } } diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html new file mode 100644 index 00000000000..4a0d60d20e3 --- /dev/null +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html @@ -0,0 +1,32 @@ + + + {{ "decryptionError" | i18n }} +
    +

    + {{ "couldNotDecryptVaultItemsBelow" | i18n }} + {{ + "contactCSToAvoidDataLossPart1" | i18n + }} + {{ "contactCSToAvoidDataLossPart2" | i18n }} +

    +
      +
    • + {{ id }} +
    • +
    +
    + + + + diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts new file mode 100644 index 00000000000..c183c4bb246 --- /dev/null +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts @@ -0,0 +1,59 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { + AnchorLinkDirective, + AsyncActionsModule, + ButtonModule, + DialogModule, + DialogService, + TypographyModule, +} from "@bitwarden/components"; + +export type DecryptionFailureDialogParams = { + cipherIds: CipherId[]; +}; + +@Component({ + standalone: true, + selector: "vault-decryption-failure-dialog", + templateUrl: "./decryption-failure-dialog.component.html", + imports: [ + DialogModule, + CommonModule, + TypographyModule, + JslibModule, + AsyncActionsModule, + ButtonModule, + AnchorLinkDirective, + ], +}) +export class DecryptionFailureDialogComponent { + protected dialogRef = inject(DialogRef); + protected params = inject(DIALOG_DATA); + protected platformUtilsService = inject(PlatformUtilsService); + + selectText(element: HTMLElement) { + const selection = window.getSelection(); + if (selection == null) { + return; + } + selection.removeAllRanges(); + const range = document.createRange(); + range.selectNodeContents(element); + selection.addRange(range); + } + + openContactSupport(event: Event) { + event.preventDefault(); + this.platformUtilsService.launchUri("https://bitwarden.com/contact"); + } + + static open(dialogService: DialogService, params: DecryptionFailureDialogParams) { + return dialogService.open(DecryptionFailureDialogComponent, { data: params }); + } +} diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index b22ced3d899..a47cdc8d1d5 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -98,6 +98,8 @@ export class DownloadAttachmentComponent { fileName: this.attachment.fileName, blobData: decBuf, }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.toastService.showToast({ variant: "error", diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html index 316df3aed17..9d7808379d3 100644 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html @@ -1,6 +1,9 @@

    {{ "newDeviceVerificationNoticeContentPage1" | i18n }} + + {{ "learnMore" | i18n }}. +

    { const doesNotHaveEmailAccess = this.formGroup.controls.hasEmailAccess.value === 0; @@ -111,4 +121,10 @@ export class NewDeviceVerificationNoticePageOneComponent implements OnInit { await this.router.navigate(["/vault"]); }; + + navigateToNewDeviceVerificationHelp(event: Event) { + event.preventDefault(); + + this.platformUtilsService.launchUri("https://bitwarden.com/help/new-device-verification/"); + } } diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html index 66a61f3b8df..1129e4750d2 100644 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.html @@ -35,7 +35,14 @@ diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts index b3634dcc28f..a2d958d53ff 100644 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-two.component.ts @@ -1,5 +1,6 @@ +import { LiveAnnouncer } from "@angular/cdk/a11y"; import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { AfterViewInit, Component, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, Observable } from "rxjs"; @@ -12,6 +13,7 @@ import { Environment, EnvironmentService, } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, LinkModule, TypographyModule } from "@bitwarden/components"; @@ -24,7 +26,7 @@ import { NewDeviceVerificationNoticeService } from "../../services/new-device-ve templateUrl: "./new-device-verification-notice-page-two.component.html", imports: [CommonModule, JslibModule, TypographyModule, ButtonModule, LinkModule], }) -export class NewDeviceVerificationNoticePageTwoComponent implements OnInit { +export class NewDeviceVerificationNoticePageTwoComponent implements OnInit, AfterViewInit { protected isWeb: boolean; protected isDesktop: boolean; protected permanentFlagEnabled = false; @@ -39,6 +41,8 @@ export class NewDeviceVerificationNoticePageTwoComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService, private configService: ConfigService, + private liveAnnouncer: LiveAnnouncer, + private i18nService: I18nService, ) { this.isWeb = this.platformUtilsService.getClientType() === ClientType.Web; this.isDesktop = this.platformUtilsService.getClientType() === ClientType.Desktop; @@ -56,6 +60,10 @@ export class NewDeviceVerificationNoticePageTwoComponent implements OnInit { this.currentUserId = currentAcct.id; } + ngAfterViewInit() { + void this.liveAnnouncer.announce(this.i18nService.t("setupTwoStepLogin"), "polite"); + } + async navigateToTwoStepLogin(event: Event) { event.preventDefault(); diff --git a/libs/vault/src/components/org-icon.directive.ts b/libs/vault/src/components/org-icon.directive.ts index bcf42503760..69a19b46c65 100644 --- a/libs/vault/src/components/org-icon.directive.ts +++ b/libs/vault/src/components/org-icon.directive.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Directive, ElementRef, HostBinding, Input, Renderer2 } from "@angular/core"; import { ProductTierType } from "@bitwarden/common/billing/enums"; @@ -11,7 +9,7 @@ export type OrgIconSize = "default" | "small" | "large"; selector: "[appOrgIcon]", }) export class OrgIconDirective { - @Input({ required: true }) tierType: ProductTierType; + @Input({ required: true }) tierType!: ProductTierType; @Input() size?: OrgIconSize = "default"; constructor( diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts index 461cc734d76..fb90df4d6ac 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -10,6 +10,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ColorPasswordModule, ItemModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; import { PasswordHistoryViewComponent } from "./password-history-view.component"; diff --git a/libs/vault/src/components/password-reprompt.component.ts b/libs/vault/src/components/password-reprompt.component.ts index acedcd79b81..6898d411719 100644 --- a/libs/vault/src/components/password-reprompt.component.ts +++ b/libs/vault/src/components/password-reprompt.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { DialogRef } from "@angular/cdk/dialog"; import { Component } from "@angular/core"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; @@ -15,6 +13,7 @@ import { DialogModule, FormFieldModule, IconButtonModule, + ToastService, } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -47,10 +46,17 @@ export class PasswordRepromptComponent { protected i18nService: I18nService, protected formBuilder: FormBuilder, protected dialogRef: DialogRef, + private toastService: ToastService, protected accountService: AccountService, ) {} submit = async () => { + // Exit early when a master password is not provided. + // The form field required error will be shown to users in these cases. + if (!this.formGroup.value.masterPassword) { + return; + } + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); if (userId == null) { @@ -68,11 +74,11 @@ export class PasswordRepromptComponent { userId, )) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); return; } diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 0112de44241..23143fa2309 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -16,5 +16,8 @@ export { DownloadAttachmentComponent } from "./components/download-attachment/do export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component"; export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component"; export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; +export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component"; export * as VaultIcons from "./icons"; + +export * from "./tasks"; diff --git a/libs/vault/src/services/copy-cipher-field.service.spec.ts b/libs/vault/src/services/copy-cipher-field.service.spec.ts index fa148b0e2e3..5a273c0828f 100644 --- a/libs/vault/src/services/copy-cipher-field.service.spec.ts +++ b/libs/vault/src/services/copy-cipher-field.service.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -22,6 +23,8 @@ describe("CopyCipherFieldService", () => { let totpService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; + let accountService: MockProxy; + const userId = "userId"; beforeEach(() => { platformUtilsService = mock(); @@ -31,6 +34,9 @@ describe("CopyCipherFieldService", () => { totpService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); + accountService = mock(); + + accountService.activeAccount$ = of({ id: userId } as Account); service = new CopyCipherFieldService( platformUtilsService, @@ -40,6 +46,7 @@ describe("CopyCipherFieldService", () => { totpService, i18nService, billingAccountProfileStateService, + accountService, ); }); @@ -76,7 +83,7 @@ describe("CopyCipherFieldService", () => { expect(toastService.showToast).toHaveBeenCalledWith({ variant: "success", message: "Username copied", - title: null, + title: "", }); expect(i18nService.t).toHaveBeenCalledWith("username"); expect(i18nService.t).toHaveBeenCalledWith("valueCopied", "Username"); @@ -128,12 +135,15 @@ describe("CopyCipherFieldService", () => { }); it("should get TOTP code when allowed from premium", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); totpService.getCode.mockResolvedValue("123456"); const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeTruthy(); expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy); expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456"); + expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith( + userId, + ); }); it("should get TOTP code when allowed from organization", async () => { @@ -146,11 +156,14 @@ describe("CopyCipherFieldService", () => { }); it("should return early when the user is not allowed to use TOTP", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeFalsy(); expect(totpService.getCode).not.toHaveBeenCalled(); expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled(); + expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith( + userId, + ); }); it("should return early when TOTP is not set", async () => { diff --git a/libs/vault/src/services/copy-cipher-field.service.ts b/libs/vault/src/services/copy-cipher-field.service.ts index 1b3bb6c0a8c..2805f3e7541 100644 --- a/libs/vault/src/services/copy-cipher-field.service.ts +++ b/libs/vault/src/services/copy-cipher-field.service.ts @@ -1,9 +1,8 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -89,6 +88,7 @@ export class CopyCipherFieldService { private totpService: TotpService, private i18nService: I18nService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} /** @@ -131,7 +131,7 @@ export class CopyCipherFieldService { this.toastService.showToast({ variant: "success", message: this.i18nService.t("valueCopied", this.i18nService.t(action.typeI18nKey)), - title: null, + title: "", }); if (action.event !== undefined) { @@ -150,10 +150,16 @@ export class CopyCipherFieldService { * Determines if TOTP generation is allowed for a cipher and user. */ async totpAllowed(cipher: CipherView): Promise { + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (!activeAccount?.id) { + return false; + } return ( (cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || - (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$))) + (await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id), + ))) ); } } diff --git a/libs/vault/src/services/new-device-verification-notice.service.ts b/libs/vault/src/services/new-device-verification-notice.service.ts index bb096ff0c2c..a925cf09ec3 100644 --- a/libs/vault/src/services/new-device-verification-notice.service.ts +++ b/libs/vault/src/services/new-device-verification-notice.service.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { Jsonify } from "type-fest"; @@ -17,8 +15,8 @@ import { UserId } from "@bitwarden/common/types/guid"; // If a user dismisses the notice, use "last_dismissal" to wait 7 days before re-prompting // permanent_dismissal will be checked if the user should never see the notice again export class NewDeviceVerificationNotice { - last_dismissal: Date; - permanent_dismissal: boolean; + last_dismissal: Date | null = null; + permanent_dismissal: boolean | null = null; constructor(obj: Partial) { if (obj == null) { @@ -52,12 +50,12 @@ export class NewDeviceVerificationNoticeService { return this.stateProvider.getUser(userId, NEW_DEVICE_VERIFICATION_NOTICE_KEY); } - noticeState$(userId: UserId): Observable { + noticeState$(userId: UserId): Observable { return this.noticeState(userId).state$; } async updateNewDeviceVerificationNoticeState( - userId: UserId | null, + userId: UserId, newState: NewDeviceVerificationNotice, ): Promise { await this.noticeState(userId).update(() => { diff --git a/libs/vault/src/services/password-reprompt.service.spec.ts b/libs/vault/src/services/password-reprompt.service.spec.ts index 300b98215c2..7d0a834b2d6 100644 --- a/libs/vault/src/services/password-reprompt.service.spec.ts +++ b/libs/vault/src/services/password-reprompt.service.spec.ts @@ -1,5 +1,7 @@ import { MockProxy, mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserVerificationService } from "@bitwarden/common/src/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DialogService } from "@bitwarden/components"; diff --git a/libs/vault/src/tasks/abstractions/task.service.ts b/libs/vault/src/tasks/abstractions/task.service.ts new file mode 100644 index 00000000000..3d6ae72900f --- /dev/null +++ b/libs/vault/src/tasks/abstractions/task.service.ts @@ -0,0 +1,45 @@ +import { Observable } from "rxjs"; + +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { SecurityTask } from "@bitwarden/vault"; + +export abstract class TaskService { + /** + * Observable indicating if tasks are enabled for a given user. + * + * @remarks Internally, this checks the user's organization details to determine if tasks are enabled. + * @param userId + */ + abstract tasksEnabled$(userId: UserId): Observable; + + /** + * Observable of all tasks for a given user. + * @param userId + */ + abstract tasks$(userId: UserId): Observable; + + /** + * Observable of pending tasks for a given user. + * @param userId + */ + abstract pendingTasks$(userId: UserId): Observable; + + /** + * Retrieves tasks from the API for a given user and updates the local state. + * @param userId + */ + abstract refreshTasks(userId: UserId): Promise; + + /** + * Clears all the tasks from state for the given user. + * @param userId + */ + abstract clear(userId: UserId): Promise; + + /** + * Marks a task as complete in local state and updates the server. + * @param taskId - The ID of the task to mark as complete. + * @param userId - The user who is completing the task. + */ + abstract markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise; +} diff --git a/libs/vault/src/tasks/enums/index.ts b/libs/vault/src/tasks/enums/index.ts new file mode 100644 index 00000000000..c1a5e81d877 --- /dev/null +++ b/libs/vault/src/tasks/enums/index.ts @@ -0,0 +1,2 @@ +export * from "./security-task-status.enum"; +export * from "./security-task-type.enum"; diff --git a/libs/vault/src/tasks/enums/security-task-status.enum.ts b/libs/vault/src/tasks/enums/security-task-status.enum.ts new file mode 100644 index 00000000000..1c6e7decc20 --- /dev/null +++ b/libs/vault/src/tasks/enums/security-task-status.enum.ts @@ -0,0 +1,11 @@ +export enum SecurityTaskStatus { + /** + * Default status for newly created tasks that have not been completed. + */ + Pending = 0, + + /** + * Status when a task is considered complete and has no remaining actions + */ + Completed = 1, +} diff --git a/libs/vault/src/tasks/enums/security-task-type.enum.ts b/libs/vault/src/tasks/enums/security-task-type.enum.ts new file mode 100644 index 00000000000..264cd88696b --- /dev/null +++ b/libs/vault/src/tasks/enums/security-task-type.enum.ts @@ -0,0 +1,6 @@ +export enum SecurityTaskType { + /** + * Task to update a cipher's password that was found to be at-risk by an administrator + */ + UpdateAtRiskCredential = 0, +} diff --git a/libs/vault/src/tasks/index.ts b/libs/vault/src/tasks/index.ts new file mode 100644 index 00000000000..a0339c6d8f4 --- /dev/null +++ b/libs/vault/src/tasks/index.ts @@ -0,0 +1,5 @@ +export * from "./enums"; +export * from "./models"; + +export * from "./abstractions/task.service"; +export * from "./services/default-task.service"; diff --git a/libs/vault/src/tasks/models/index.ts b/libs/vault/src/tasks/models/index.ts new file mode 100644 index 00000000000..7e31c136629 --- /dev/null +++ b/libs/vault/src/tasks/models/index.ts @@ -0,0 +1 @@ +export * from "./security-task"; diff --git a/libs/vault/src/tasks/models/security-task.data.ts b/libs/vault/src/tasks/models/security-task.data.ts new file mode 100644 index 00000000000..e2d9cc76c0a --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.data.ts @@ -0,0 +1,34 @@ +import { Jsonify } from "type-fest"; + +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +import { SecurityTaskResponse } from "./security-task.response"; + +export class SecurityTaskData { + id: SecurityTaskId; + organizationId: OrganizationId; + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(response: SecurityTaskResponse) { + this.id = response.id; + this.organizationId = response.organizationId; + this.cipherId = response.cipherId; + this.type = response.type; + this.status = response.status; + this.creationDate = response.creationDate; + this.revisionDate = response.revisionDate; + } + + static fromJSON(obj: Jsonify) { + return Object.assign(new SecurityTaskData({} as SecurityTaskResponse), obj, { + creationDate: new Date(obj.creationDate), + revisionDate: new Date(obj.revisionDate), + }); + } +} diff --git a/libs/vault/src/tasks/models/security-task.response.ts b/libs/vault/src/tasks/models/security-task.response.ts new file mode 100644 index 00000000000..2a335eb5d2f --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.response.ts @@ -0,0 +1,28 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +export class SecurityTaskResponse extends BaseResponse { + id: SecurityTaskId; + organizationId: OrganizationId; + /** + * Optional cipherId for tasks that are related to a cipher. + */ + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.organizationId = this.getResponseProperty("OrganizationId"); + this.cipherId = this.getResponseProperty("CipherId") || undefined; + this.type = this.getResponseProperty("Type"); + this.status = this.getResponseProperty("Status"); + this.creationDate = this.getResponseProperty("CreationDate"); + this.revisionDate = this.getResponseProperty("RevisionDate"); + } +} diff --git a/libs/vault/src/tasks/models/security-task.ts b/libs/vault/src/tasks/models/security-task.ts new file mode 100644 index 00000000000..42adb951945 --- /dev/null +++ b/libs/vault/src/tasks/models/security-task.ts @@ -0,0 +1,28 @@ +import { CipherId, OrganizationId, SecurityTaskId } from "@bitwarden/common/types/guid"; + +import { SecurityTaskStatus, SecurityTaskType } from "../enums"; + +import { SecurityTaskData } from "./security-task.data"; + +export class SecurityTask { + id: SecurityTaskId; + organizationId: OrganizationId; + /** + * Optional cipherId for tasks that are related to a cipher. + */ + cipherId: CipherId | undefined; + type: SecurityTaskType; + status: SecurityTaskStatus; + creationDate: Date; + revisionDate: Date; + + constructor(obj: SecurityTaskData) { + this.id = obj.id; + this.organizationId = obj.organizationId; + this.cipherId = obj.cipherId; + this.type = obj.type; + this.status = obj.status; + this.creationDate = obj.creationDate; + this.revisionDate = obj.revisionDate; + } +} diff --git a/libs/vault/src/tasks/services/default-task.service.spec.ts b/libs/vault/src/tasks/services/default-task.service.spec.ts new file mode 100644 index 00000000000..850d0bcc2b8 --- /dev/null +++ b/libs/vault/src/tasks/services/default-task.service.spec.ts @@ -0,0 +1,261 @@ +import { TestBed } from "@angular/core/testing"; +import { BehaviorSubject, firstValueFrom } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { DefaultTaskService, SecurityTaskStatus } from "@bitwarden/vault"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../common/spec"; +import { SecurityTaskData } from "../models/security-task.data"; +import { SecurityTaskResponse } from "../models/security-task.response"; +import { SECURITY_TASKS } from "../state/security-task.state"; + +describe("Default task service", () => { + let fakeStateProvider: FakeStateProvider; + + const mockApiSend = jest.fn(); + const mockGetAllOrgs$ = jest.fn(); + + let testBed: TestBed; + + beforeEach(async () => { + mockApiSend.mockClear(); + mockGetAllOrgs$.mockClear(); + + fakeStateProvider = new FakeStateProvider(mockAccountServiceWith("user-id" as UserId)); + testBed = TestBed.configureTestingModule({ + imports: [], + providers: [ + DefaultTaskService, + { + provide: StateProvider, + useValue: fakeStateProvider, + }, + { + provide: ApiService, + useValue: { + send: mockApiSend, + }, + }, + { + provide: OrganizationService, + useValue: { + getAll$: mockGetAllOrgs$, + }, + }, + ], + }); + }); + + describe("tasksEnabled$", () => { + it("should emit true if any organization uses risk insights", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + { + useRiskInsights: true, + }, + ] as Organization[]), + ); + + const { tasksEnabled$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasksEnabled$("user-id" as UserId)); + + expect(result).toBe(true); + }); + + it("should emit false if no organization uses risk insights", async () => { + mockGetAllOrgs$.mockReturnValue( + new BehaviorSubject([ + { + useRiskInsights: false, + }, + { + useRiskInsights: false, + }, + ] as Organization[]), + ); + + const { tasksEnabled$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasksEnabled$("user-id" as UserId)); + + expect(result).toBe(false); + }); + }); + + describe("tasks$", () => { + it("should fetch tasks from the API when the state is null", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null); + + const { tasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + }); + + it("should use the tasks from state when not null", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const { tasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(tasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(mockApiSend).not.toHaveBeenCalled(); + }); + + it("should share the same observable for the same user", async () => { + const { tasks$ } = testBed.inject(DefaultTaskService); + + const first = tasks$("user-id" as UserId); + const second = tasks$("user-id" as UserId); + + expect(first).toBe(second); + }); + }); + + describe("pendingTasks$", () => { + it("should filter tasks to only pending tasks", async () => { + fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "completed-task-id" as SecurityTaskId, + status: SecurityTaskStatus.Completed, + }, + { + id: "pending-task-id" as SecurityTaskId, + status: SecurityTaskStatus.Pending, + }, + ] as SecurityTaskData[]); + + const { pendingTasks$ } = testBed.inject(DefaultTaskService); + + const result = await firstValueFrom(pendingTasks$("user-id" as UserId)); + + expect(result.length).toBe(1); + expect(result[0].id).toBe("pending-task-id" as SecurityTaskId); + }); + }); + + describe("refreshTasks()", () => { + it("should fetch tasks from the API", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + const service = testBed.inject(DefaultTaskService); + + await service.refreshTasks("user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + }); + + it("should update the local state with refreshed tasks", async () => { + mockApiSend.mockResolvedValue({ + data: [ + { + id: "task-id", + }, + ] as SecurityTaskResponse[], + }); + + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, null); + + const service = testBed.inject(DefaultTaskService); + + await service.refreshTasks("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + }); + }); + + describe("clear()", () => { + it("should clear the local state for the user", async () => { + const mock = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const service = testBed.inject(DefaultTaskService); + + await service.clear("user-id" as UserId); + + expect(mock.nextMock).toHaveBeenCalledWith([]); + }); + }); + + describe("markAsComplete()", () => { + it("should send an API request to mark the task as complete", async () => { + const service = testBed.inject(DefaultTaskService); + + await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith( + "PATCH", + "/tasks/task-id/complete", + null, + true, + false, + ); + }); + + it("should refresh all tasks for the user after marking the task as complete", async () => { + mockApiSend + .mockResolvedValueOnce(null) // Mark as complete + .mockResolvedValueOnce({ + // Refresh tasks + data: [ + { + id: "new-task-id", + }, + ] as SecurityTaskResponse[], + }); + + const mockState = fakeStateProvider.singleUser.mockFor("user-id" as UserId, SECURITY_TASKS, [ + { + id: "old-task-id" as SecurityTaskId, + } as SecurityTaskData, + ]); + + const service = testBed.inject(DefaultTaskService); + + await service.markAsComplete("task-id" as SecurityTaskId, "user-id" as UserId); + + expect(mockApiSend).toHaveBeenCalledWith("GET", "/tasks", null, true, true); + expect(mockState.nextMock).toHaveBeenCalledWith([ + { + id: "new-task-id", + } as SecurityTaskData, + ]); + }); + }); +}); diff --git a/libs/vault/src/tasks/services/default-task.service.ts b/libs/vault/src/tasks/services/default-task.service.ts new file mode 100644 index 00000000000..2fc4ba3a937 --- /dev/null +++ b/libs/vault/src/tasks/services/default-task.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from "@angular/core"; +import { map, switchMap } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { SecurityTaskId, UserId } from "@bitwarden/common/types/guid"; +import { SecurityTask, SecurityTaskStatus, TaskService } from "@bitwarden/vault"; + +import { filterOutNullish, perUserCache$ } from "../../utils/observable-utilities"; +import { SecurityTaskData } from "../models/security-task.data"; +import { SecurityTaskResponse } from "../models/security-task.response"; +import { SECURITY_TASKS } from "../state/security-task.state"; + +@Injectable() +export class DefaultTaskService implements TaskService { + constructor( + private stateProvider: StateProvider, + private apiService: ApiService, + private organizationService: OrganizationService, + ) {} + + tasksEnabled$ = perUserCache$((userId) => { + return this.organizationService + .getAll$(userId) + .pipe(map((orgs) => orgs.some((o) => o.useRiskInsights))); + }); + + tasks$ = perUserCache$((userId) => { + return this.taskState(userId).state$.pipe( + switchMap(async (tasks) => { + if (tasks == null) { + await this.fetchTasksFromApi(userId); + } + return tasks; + }), + filterOutNullish(), + map((tasks) => tasks.map((t) => new SecurityTask(t))), + ); + }); + + pendingTasks$ = perUserCache$((userId) => { + return this.tasks$(userId).pipe( + map((tasks) => tasks.filter((t) => t.status === SecurityTaskStatus.Pending)), + ); + }); + + async refreshTasks(userId: UserId): Promise { + await this.fetchTasksFromApi(userId); + } + + async clear(userId: UserId): Promise { + await this.updateTaskState(userId, []); + } + + async markAsComplete(taskId: SecurityTaskId, userId: UserId): Promise { + await this.apiService.send("PATCH", `/tasks/${taskId}/complete`, null, true, false); + await this.refreshTasks(userId); + } + + /** + * Fetches the tasks from the API and updates the local state + * @param userId + * @private + */ + private async fetchTasksFromApi(userId: UserId): Promise { + const r = await this.apiService.send("GET", "/tasks", null, true, true); + const response = new ListResponse(r, SecurityTaskResponse); + + const taskData = response.data.map((t) => new SecurityTaskData(t)); + await this.updateTaskState(userId, taskData); + } + + /** + * Returns the local state for the tasks + * @param userId + * @private + */ + private taskState(userId: UserId) { + return this.stateProvider.getUser(userId, SECURITY_TASKS); + } + + /** + * Updates the local state with the provided tasks and returns the updated state + * @param userId + * @param tasks + * @private + */ + private updateTaskState(userId: UserId, tasks: SecurityTaskData[]): Promise { + return this.taskState(userId).update(() => tasks); + } +} diff --git a/libs/vault/src/tasks/state/security-task.state.ts b/libs/vault/src/tasks/state/security-task.state.ts new file mode 100644 index 00000000000..b86a891f008 --- /dev/null +++ b/libs/vault/src/tasks/state/security-task.state.ts @@ -0,0 +1,14 @@ +import { Jsonify } from "type-fest"; + +import { SECURITY_TASKS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; + +import { SecurityTaskData } from "../models/security-task.data"; + +export const SECURITY_TASKS = UserKeyDefinition.array( + SECURITY_TASKS_DISK, + "securityTasks", + { + deserializer: (task: Jsonify) => SecurityTaskData.fromJSON(task), + clearOn: ["logout", "lock"], + }, +); diff --git a/libs/vault/src/utils/observable-utilities.ts b/libs/vault/src/utils/observable-utilities.ts new file mode 100644 index 00000000000..bb559c600d3 --- /dev/null +++ b/libs/vault/src/utils/observable-utilities.ts @@ -0,0 +1,37 @@ +import { filter, Observable, OperatorFunction, shareReplay } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +/** + * Builds an observable once per userId and caches it for future requests. + * The built observables are shared among subscribers with a replay buffer size of 1. + * @param create - A function that creates an observable for a given userId. + */ +export function perUserCache$( + create: (userId: UserId) => Observable, +): (userId: UserId) => Observable { + const cache = new Map>(); + return (userId: UserId) => { + let observable = cache.get(userId); + if (!observable) { + observable = create(userId).pipe(shareReplay({ bufferSize: 1, refCount: false })); + cache.set(userId, observable); + } + return observable; + }; +} + +/** + * Strongly typed observable operator that filters out null/undefined values and adjusts the return type to + * be non-nullable. + * + * @example + * ```ts + * const source$ = of(1, null, 2, undefined, 3); + * source$.pipe(filterOutNullish()).subscribe(console.log); + * // Output: 1, 2, 3 + * ``` + */ +export function filterOutNullish(): OperatorFunction { + return filter((v): v is T => v != null); +} diff --git a/libs/vault/tsconfig.json b/libs/vault/tsconfig.json index 6004a56fb55..e1515183f22 100644 --- a/libs/vault/tsconfig.json +++ b/libs/vault/tsconfig.json @@ -1,5 +1,24 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "resolveJsonModule": true, + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-components": ["../tools/generator/components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/ui-common": ["../ui/common/src"], + "@bitwarden/vault": ["../vault/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/package-lock.json b/package-lock.json index a8ae6daf236..9d2040476c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,33 +15,34 @@ "libs/*" ], "dependencies": { - "@angular/animations": "17.3.12", - "@angular/cdk": "17.3.10", - "@angular/common": "17.3.12", - "@angular/compiler": "17.3.12", - "@angular/core": "17.3.12", - "@angular/forms": "17.3.12", - "@angular/platform-browser": "17.3.12", - "@angular/platform-browser-dynamic": "17.3.12", - "@angular/router": "17.3.12", + "@angular/animations": "18.2.13", + "@angular/cdk": "18.2.14", + "@angular/common": "18.2.13", + "@angular/compiler": "18.2.13", + "@angular/core": "18.2.13", + "@angular/forms": "18.2.13", + "@angular/platform-browser": "18.2.13", + "@angular/platform-browser-dynamic": "18.2.13", + "@angular/router": "18.2.13", "@bitwarden/sdk-internal": "0.2.0-main.38", "@electron/fuses": "1.8.0", + "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", - "@ng-select/ng-select": "12.0.7", + "@ng-select/ng-select": "13.9.1", "argon2": "0.41.1", "argon2-browser": "1.18.0", "big-integer": "1.6.52", "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", - "bufferutil": "4.0.8", + "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.39.0", - "form-data": "4.0.0", + "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", @@ -50,10 +51,11 @@ "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", + "lit": "3.2.1", "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.1", - "ngx-infinite-scroll": "17.0.1", + "ngx-infinite-scroll": "18.0.0", "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", @@ -68,26 +70,25 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.69", + "tldts": "6.1.71", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" }, "devDependencies": { - "@angular-devkit/build-angular": "17.3.11", - "@angular-eslint/eslint-plugin": "17.5.3", - "@angular-eslint/eslint-plugin-template": "17.5.3", - "@angular-eslint/schematics": "17.5.3", - "@angular-eslint/template-parser": "17.5.3", - "@angular/cli": "17.3.11", - "@angular/compiler-cli": "17.3.12", - "@angular/elements": "17.3.12", + "@angular-devkit/build-angular": "18.2.12", + "@angular-eslint/eslint-plugin": "18.4.3", + "@angular-eslint/eslint-plugin-template": "18.4.3", + "@angular-eslint/schematics": "18.4.3", + "@angular-eslint/template-parser": "18.4.3", + "@angular/cli": "18.2.12", + "@angular/compiler-cli": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", "@electron/notarize": "2.5.0", "@electron/rebuild": "3.7.1", - "@ngtools/webpack": "17.3.11", + "@ngtools/webpack": "18.2.12", "@storybook/addon-a11y": "8.4.7", "@storybook/addon-actions": "8.4.7", "@storybook/addon-designs": "8.0.4", @@ -97,6 +98,7 @@ "@storybook/angular": "8.4.7", "@storybook/manager-api": "8.4.7", "@storybook/theming": "8.4.7", + "@storybook/web-components-webpack5": "8.4.7", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.280", "@types/firefox-webext-browser": "120.0.4", @@ -111,7 +113,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.1", + "@types/node": "22.10.5", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -119,42 +121,42 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", - "chromatic": "11.20.0", - "concurrently": "9.1.0", + "chromatic": "11.20.2", + "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.2.1", + "electron": "33.3.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.3.9", - "eslint": "8.57.0", - "eslint-config-prettier": "9.1.0", - "eslint-import-resolver-typescript": "3.6.1", - "eslint-plugin-import": "2.29.1", + "eslint": "8.57.1", + "eslint-config-prettier": "10.0.1", + "eslint-import-resolver-typescript": "3.7.0", + "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", - "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.17.4", + "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-tailwindcss": "3.17.5", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", - "husky": "9.1.4", - "jest-extended": "4.0.2", + "husky": "9.1.7", + "jest-diff": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.2.8", + "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", "postcss": "8.4.49", @@ -164,14 +166,14 @@ "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.83.4", "sass-loader": "16.0.4", "storybook": "8.4.7", "style-loader": "4.0.0", - "tailwindcss": "3.4.16", + "tailwindcss": "3.4.17", "ts-jest": "29.2.2", "ts-loader": "9.5.1", - "tsconfig-paths-webpack-plugin": "4.1.0", + "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", "typescript-strict-plugin": "^2.4.4", @@ -179,8 +181,8 @@ "util": "0.12.5", "wait-on": "8.0.1", "webpack": "5.97.1", - "webpack-cli": "5.1.4", - "webpack-dev-server": "5.0.4", + "webpack-cli": "6.0.1", + "webpack-dev-server": "5.2.0", "webpack-node-externals": "3.0.0" }, "engines": { @@ -190,11 +192,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.12.3" + "version": "2025.1.1" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.12.0", + "version": "2025.1.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -204,7 +206,7 @@ "browser-hrtime": "1.1.8", "chalk": "4.1.2", "commander": "11.1.0", - "form-data": "4.0.0", + "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jsdom": "25.0.1", @@ -221,7 +223,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.69", + "tldts": "6.1.71", "zxcvbn": "4.4.2" }, "bin": { @@ -230,7 +232,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.12.2", + "version": "2025.1.4", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -244,7 +246,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.12.1" + "version": "2025.1.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -369,14 +371,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.11.tgz", - "integrity": "sha512-p+XIc/j51aI83ExNdeZwvkm1F4wkuKMGUUoj0MVUUi5E6NoiMlXYm6uU8+HbRvPBzGy5+3KOiGp3Fks0UmDSAA==", + "version": "0.1802.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.12.tgz", + "integrity": "sha512-bepVb2/GtJppYKaeW8yTGE6egmoWZ7zagFDsmBdbF+BYp+HmeoPsclARcdryBPVq68zedyTRdvhWSUTbw1AYuw==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { - "@angular-devkit/core": "18.2.11", + "@angular-devkit/core": "18.2.12", "rxjs": "7.8.1" }, "engines": { @@ -386,98 +386,96 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.11.tgz", - "integrity": "sha512-lHX5V2dSts328yvo/9E2u9QMGcvJhbEKKDDp9dBecwvIG9s+4lTOJgi9DPUE7W+AtmPcmbbhwC2JRQ/SLQhAoA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.12.tgz", + "integrity": "sha512-quVUi7eqTq9OHumQFNl9Y8t2opm8miu4rlYnuF6rbujmmBDvdUvR6trFChueRczl2p5HWqTOr6NPoDGQm8AyNw==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.11", - "@angular-devkit/build-webpack": "0.1703.11", - "@angular-devkit/core": "17.3.11", - "@babel/core": "7.24.0", - "@babel/generator": "7.23.6", - "@babel/helper-annotate-as-pure": "7.22.5", - "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.9", - "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.24.0", - "@babel/preset-env": "7.24.0", - "@babel/runtime": "7.24.0", - "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.11", + "@angular-devkit/architect": "0.1802.12", + "@angular-devkit/build-webpack": "0.1802.12", + "@angular-devkit/core": "18.2.12", + "@angular/build": "18.2.12", + "@babel/core": "7.25.2", + "@babel/generator": "7.25.0", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.25.0", + "@babel/plugin-transform-async-to-generator": "7.24.7", + "@babel/plugin-transform-runtime": "7.24.7", + "@babel/preset-env": "7.25.3", + "@babel/runtime": "7.25.0", + "@discoveryjs/json-ext": "0.6.1", + "@ngtools/webpack": "18.2.12", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.20", "babel-loader": "9.1.3", - "babel-plugin-istanbul": "6.1.1", "browserslist": "^4.21.5", - "copy-webpack-plugin": "11.0.0", - "critters": "0.0.22", - "css-loader": "6.10.0", - "esbuild-wasm": "0.20.1", + "copy-webpack-plugin": "12.0.2", + "critters": "0.0.24", + "css-loader": "7.1.2", + "esbuild-wasm": "0.23.0", "fast-glob": "3.3.2", - "http-proxy-middleware": "2.0.7", - "https-proxy-agent": "7.0.4", - "inquirer": "9.2.15", - "jsonc-parser": "3.2.1", + "http-proxy-middleware": "3.0.3", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", "karma-source-map-support": "1.4.0", "less": "4.2.0", - "less-loader": "11.1.0", + "less-loader": "12.2.0", "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.1", - "magic-string": "0.30.8", - "mini-css-extract-plugin": "2.8.1", + "loader-utils": "3.3.1", + "magic-string": "0.30.11", + "mini-css-extract-plugin": "2.9.0", "mrmime": "2.0.0", - "open": "8.4.2", + "open": "10.1.0", "ora": "5.4.1", "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.1", - "piscina": "4.4.0", - "postcss": "8.4.35", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "postcss": "8.4.41", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.71.1", - "sass-loader": "14.1.1", - "semver": "7.6.0", + "sass": "1.77.6", + "sass-loader": "16.0.0", + "semver": "7.6.3", "source-map-loader": "5.0.0", "source-map-support": "0.5.21", - "terser": "5.29.1", + "terser": "5.31.6", "tree-kill": "1.2.2", - "tslib": "2.6.2", - "undici": "6.11.1", - "vite": "5.1.8", - "watchpack": "2.4.0", + "tslib": "2.6.3", + "vite": "5.4.6", + "watchpack": "2.4.1", "webpack": "5.94.0", - "webpack-dev-middleware": "6.1.2", - "webpack-dev-server": "4.15.1", - "webpack-merge": "5.10.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.0.4", + "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.20.1" + "esbuild": "0.23.0" }, "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "@angular/localize": "^17.0.0", - "@angular/platform-server": "^17.0.0", - "@angular/service-worker": "^17.0.0", + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", "@web/test-runner": "^0.18.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", - "ng-packagr": "^17.0.0", + "ng-packagr": "^18.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.2 <5.5" + "typescript": ">=5.4 <5.6" }, "peerDependenciesMeta": { "@angular/localize": { @@ -515,87 +513,22 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { - "version": "0.1703.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.11.tgz", - "integrity": "sha512-YNasVZk4rYdcM6M+KRH8PUBhVyJfqzUYLpO98GgRokW+taIDgifckSlmfDZzQRbw45qiwei1IKCLqcpC8nM5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "17.3.11", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.11.tgz", - "integrity": "sha512-qbCiiHuoVkD7CtLyWoRi/Vzz6nrEztpF5XIyWUcQu67An1VlxbMTE4yoSQiURjCQMnB/JvS1GPVed7wOq3SJ/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/architect": "0.1703.11", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -621,27 +554,28 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@babel/preset-env": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", - "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", + "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/compat-data": "^7.25.2", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -653,59 +587,60 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.24.0", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -720,102 +655,17 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@angular-devkit/build-angular/node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "node_modules/@angular-devkit/build-angular/node_modules/@discoveryjs/json-ext": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", + "integrity": "sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA==", "dev": true, - "license": "MIT" - }, - "node_modules/@angular-devkit/build-angular/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=14.17.0" } }, "node_modules/@angular-devkit/build-angular/node_modules/babel-loader": { @@ -836,40 +686,6 @@ "webpack": ">=5" } }, - "node_modules/@angular-devkit/build-angular/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -877,65 +693,16 @@ "dev": true, "license": "MIT" }, - "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "node_modules/@angular-devkit/build-angular/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, "engines": { - "node": ">= 14.15.0" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/css-loader": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", - "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@angular-devkit/build-angular/node_modules/eslint-scope": { @@ -962,100 +729,21 @@ "node": ">=4.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/@angular-devkit/build-angular/node_modules/http-proxy-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", "dev": true, - "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/inquirer": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", - "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ljharb/through": "^2.3.12", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^3.2.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@angular-devkit/build-angular/node_modules/ipaddr.js": { @@ -1063,11 +751,41 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10" } }, + "node_modules/@angular-devkit/build-angular/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -1075,12 +793,30 @@ "dev": true, "license": "MIT" }, - "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", - "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "node_modules/@angular-devkit/build-angular/node_modules/memfs": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.1.tgz", + "integrity": "sha512-Fq5CMEth+2iprLJ5mNizRcWuiwRZYjNkUD0zKk224jZunE9CRacTRDK8QLALbMBlNX2y3nY6lKZbesCwDwacig==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", "dev": true, - "license": "MIT", "dependencies": { "schema-utils": "^4.0.0", "tapable": "^2.2.1" @@ -1096,47 +832,28 @@ "webpack": "^5.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@angular-devkit/build-angular/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" }, "engines": { - "node": "*" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@angular-devkit/build-angular/node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -1152,59 +869,35 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, - "node_modules/@angular-devkit/build-angular/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dev": true, - "license": "ISC", "dependencies": { - "glob": "^7.1.3" + "glob": "^10.3.7" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@angular-devkit/build-angular/node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/sass": { - "version": "1.71.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", - "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", "dev": true, - "license": "MIT", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -1218,11 +911,10 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/sass-loader": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", - "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", "dev": true, - "license": "MIT", "dependencies": { "neo-async": "^2.6.2" }, @@ -1258,19 +950,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@angular-devkit/build-angular/node_modules/webpack": { "version": "5.94.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", @@ -1318,56 +997,84 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -1378,28 +1085,28 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", "dev": true, - "license": "MIT", "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" }, "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=12.0.0" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } } }, "node_modules/@angular-devkit/build-angular/node_modules/webpack/node_modules/ajv": { @@ -1469,13 +1176,30 @@ "node": ">=10.13.0" } }, - "node_modules/@angular-devkit/core": { - "version": "18.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.11.tgz", - "integrity": "sha512-H9P1shRGigORWJHUY2BRa2YurT+DVminrhuaYHsbhXBRsPmgB2Dx/30YLTnC1s5XmR9QIRUCsg/d3kyT1wd5Zg==", + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1802.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.12.tgz", + "integrity": "sha512-0Z3fdbZVRnjYWE2/VYyfy+uieY+6YZyEp4ylzklVkc+fmLNsnz4Zw6cK1LzzcBqAwKIyh1IdW20Cg7o8b0sONA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1802.12", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/core": { + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.12.tgz", + "integrity": "sha512-NtB6ypsaDyPE6/fqWOdfTmACs+yK5RqfH5tStEzWFeeDsIEDYKsJ06ypuRep7qTjYus5Rmttk0Ds+cFgz8JdUQ==", "dev": true, - "license": "MIT", - "peer": true, "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", @@ -1498,219 +1222,682 @@ } } }, - "node_modules/@angular-devkit/core/node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@angular-devkit/core/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@angular-devkit/schematics": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", - "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.12.tgz", + "integrity": "sha512-mMea9txHbnCX5lXLHlo0RAgfhFHDio45/jMsREM2PA8UtVf2S8ltXz7ZwUrUyMQRv8vaSfn4ijDstF4hDMnRgQ==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.11", - "jsonc-parser": "3.2.1", - "magic-string": "0.30.8", + "@angular-devkit/core": "18.2.12", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", "ora": "5.4.1", "rxjs": "7.8.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/schematics/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/schematics/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "17.5.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.5.3.tgz", - "integrity": "sha512-x9jZ6mME9wxumErPGonWERXX/9TJ7mzEkQhOKt3BxBFm0sy9XQqLMAenp1PBSg3RF3rH7EEVdB2+jb75RtHp0g==", - "dev": true, - "license": "MIT" + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.4.3.tgz", + "integrity": "sha512-zdrA8mR98X+U4YgHzUKmivRU+PxzwOL/j8G7eTOvBuq8GPzsP+hvak+tyxlgeGm9HsvpFj9ERHLtJ0xDUPs8fg==", + "dev": true }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "17.5.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.5.3.tgz", - "integrity": "sha512-2gMRZ+SkiygrPDtCJwMfjmwIFOcvxxC4NRX/MqRo6udsa0gtqPrc8acRbwrmAXlullmhzmaeUfkHpGDSzW8pFw==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-18.4.3.tgz", + "integrity": "sha512-AyJbupiwTBR81P6T59v+aULEnPpZBCBxL2S5QFWfAhNCwWhcof4GihvdK2Z87yhvzDGeAzUFSWl/beJfeFa+PA==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.5.3", - "@angular-eslint/utils": "17.5.3", - "@typescript-eslint/utils": "7.11.0" + "@angular-eslint/bundled-angular-compiler": "18.4.3", + "@angular-eslint/utils": "18.4.3" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "17.5.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.5.3.tgz", - "integrity": "sha512-RkRFagxqBPV2xdNyeQQROUm6I1Izto1Z3Wy73lCk2zq1RhVgbznniH/epmOIE8PMkHmMKmZ765FV++J/90p4Ig==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.4.3.tgz", + "integrity": "sha512-ijGlX2N01ayMXTpeQivOA31AszO8OEbu9ZQUCxnu9AyMMhxyi2q50bujRChAvN9YXQfdQtbxuajxV6+aiWb5BQ==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.5.3", - "@angular-eslint/utils": "17.5.3", - "@typescript-eslint/type-utils": "7.11.0", - "@typescript-eslint/utils": "7.11.0", - "aria-query": "5.3.0", - "axobject-query": "4.0.0" + "@angular-eslint/bundled-angular-compiler": "18.4.3", + "@angular-eslint/utils": "18.4.3", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, - "node_modules/@angular-eslint/schematics": { - "version": "17.5.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.5.3.tgz", - "integrity": "sha512-a0MlOjNLIM18l/66S+CzhANQR3QH3jDUa1MC50E4KBf1mwjQyfqd6RdfbOTMDjgFlPrfB+5JvoWOHHGj7FFM1A==", + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-18.4.3.tgz", + "integrity": "sha512-D5maKn5e6n58+8n7jLFLD4g+RGPOPeDSsvPc1sqial5tEKLxAJQJS9WZ28oef3bhkob6C60D+1H0mMmEEVvyVA==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-eslint/eslint-plugin": "17.5.3", - "@angular-eslint/eslint-plugin-template": "17.5.3", - "ignore": "5.3.1", - "strip-json-comments": "3.1.1", - "tmp": "0.2.3" - }, - "peerDependencies": { - "@angular/cli": ">= 17.0.0 < 18.0.0" + "@angular-devkit/core": ">= 18.0.0 < 19.0.0", + "@angular-devkit/schematics": ">= 18.0.0 < 19.0.0", + "@angular-eslint/eslint-plugin": "18.4.3", + "@angular-eslint/eslint-plugin-template": "18.4.3", + "ignore": "6.0.2", + "semver": "7.6.3", + "strip-json-comments": "3.1.1" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "engines": { + "node": ">= 4" } }, "node_modules/@angular-eslint/template-parser": { - "version": "17.5.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.5.3.tgz", - "integrity": "sha512-NYybOsMkJUtFOW2JWALicipq0kK5+jGwA1MYyRoXjdbDlXltHUb9qkXj7p0fE6uRutBGXDl4288s8g/fZCnAIA==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-18.4.3.tgz", + "integrity": "sha512-JZMPtEB8yNip3kg4WDEWQyObSo2Hwf+opq2ElYuwe85GQkGhfJSJ2CQYo4FSwd+c5MUQAqESNRg9QqGYauDsiw==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.5.3", - "eslint-scope": "^8.0.0" + "@angular-eslint/bundled-angular-compiler": "18.4.3", + "eslint-scope": "^8.0.2" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/utils": { - "version": "17.5.3", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.5.3.tgz", - "integrity": "sha512-0nNm1FUOLhVHrdK2PP5dZCYYVmTIkEJ4CmlwpuC4JtCLbD5XAHQpY/ZW5Ff5n1b7KfJt1Zy//jlhkkIaw3LaBQ==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-18.4.3.tgz", + "integrity": "sha512-w0bJ9+ELAEiPBSTPPm9bvDngfu1d8JbzUhvs2vU+z7sIz/HMwUZT5S4naypj2kNN0gZYGYrW0lt+HIbW87zTAQ==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.5.3", - "@typescript-eslint/utils": "7.11.0" + "@angular-eslint/bundled-angular-compiler": "18.4.3" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": "*" } }, "node_modules/@angular/animations": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz", - "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.13.tgz", + "integrity": "sha512-rG5J5Ek5Hg+Tz2NjkNOaG6PupiNK/lPfophXpsR1t/nWujqnMWX2krahD/i6kgD+jNWNKCJCYSOVvCx/BHOtKA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "17.3.12" + "@angular/core": "18.2.13" + } + }, + "node_modules/@angular/build": { + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.12.tgz", + "integrity": "sha512-4Ohz+OSILoL+cCAQ4UTiCT5v6pctu3fXNoNpTEUK46OmxELk9jDITO5rNyNS7TxBn9wY69kjX5VcDf7MenquFQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.12", + "@babel/core": "7.25.2", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.24.7", + "@inquirer/confirm": "3.1.22", + "@vitejs/plugin-basic-ssl": "1.1.0", + "browserslist": "^4.23.0", + "critters": "0.0.24", + "esbuild": "0.23.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "listr2": "8.2.4", + "lmdb": "3.0.13", + "magic-string": "0.30.11", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "rollup": "4.22.4", + "sass": "1.77.6", + "semver": "7.6.3", + "vite": "5.4.6", + "watchpack": "2.4.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/build/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/build/node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@angular/build/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@angular/build/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@angular/build/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/build/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@angular/build/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/@angular/build/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/@angular/build/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/build/node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@angular/build/node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular/build/node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@angular/build/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/@angular/build/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/build/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@angular/build/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@angular/cdk": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", - "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", - "license": "MIT", + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.14.tgz", + "integrity": "sha512-vDyOh1lwjfVk9OqoroZAP8pf3xxKUvyl+TVR8nJxL4c5fOfUFkD7l94HaanqKSRwJcI2xiztuu92IVoHn8T33Q==", "dependencies": { "tslib": "^2.3.0" }, @@ -1718,34 +1905,32 @@ "parse5": "^7.1.2" }, "peerDependencies": { - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/common": "^18.0.0 || ^19.0.0", + "@angular/core": "^18.0.0 || ^19.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/cli": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.11.tgz", - "integrity": "sha512-8R9LwAGL8hGAWJ4mNG9ZPUrBUzIdmst0Ldua6RJJ+PrqgjX+8IbO+lNnfrOY/XY+Z3LXbCEJflL26f9czCvTPQ==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.12.tgz", + "integrity": "sha512-xhuZ/b7IhqNw1MgXf+arWf4x+GfUSt/IwbdWU4+CO8A7h0Y46zQywouP/KUK3cMQZfVdHdciTBvlpF3vFacA6Q==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1703.11", - "@angular-devkit/core": "17.3.11", - "@angular-devkit/schematics": "17.3.11", - "@schematics/angular": "17.3.11", + "@angular-devkit/architect": "0.1802.12", + "@angular-devkit/core": "18.2.12", + "@angular-devkit/schematics": "18.2.12", + "@inquirer/prompts": "5.3.8", + "@listr2/prompt-adapter-inquirer": "2.0.15", + "@schematics/angular": "18.2.12", "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.3", - "ini": "4.1.2", - "inquirer": "9.2.15", - "jsonc-parser": "3.2.1", - "npm-package-arg": "11.0.1", - "npm-pick-manifest": "9.0.0", - "open": "8.4.2", - "ora": "5.4.1", - "pacote": "17.0.6", + "ini": "4.1.3", + "jsonc-parser": "3.3.1", + "listr2": "8.2.4", + "npm-package-arg": "11.0.3", + "npm-pick-manifest": "9.1.0", + "pacote": "18.0.6", "resolve": "1.22.8", - "semver": "7.6.0", + "semver": "7.6.3", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -1753,189 +1938,184 @@ "ng": "bin/ng.js" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.1703.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.11.tgz", - "integrity": "sha512-YNasVZk4rYdcM6M+KRH8PUBhVyJfqzUYLpO98GgRokW+taIDgifckSlmfDZzQRbw45qiwei1IKCLqcpC8nM5Tw==", + "node_modules/@angular/cli/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "17.3.11", - "rxjs": "7.8.1" - }, "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular/cli/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "node": ">=12" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@angular/cli/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "node_modules/@angular/cli/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@angular/cli/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@angular/cli/node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "node_modules/@angular/cli/node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@angular/cli/node_modules/inquirer": { - "version": "9.2.15", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", - "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", - "dev": true, - "license": "MIT", "dependencies": { - "@ljharb/through": "^2.3.12", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^3.2.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@angular/cli/node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "node_modules/@angular/cli/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/@angular/cli/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/@angular/cli/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, - "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@angular/cli/node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "node_modules/@angular/cli/node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", "dev": true, - "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">=18.0.0" + } + }, + "node_modules/@angular/cli/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/@angular/cli/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular/cli/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@angular/cli/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@angular/common": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz", - "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.13.tgz", + "integrity": "sha512-4ZqrNp1PoZo7VNvW+sbSc2CB2axP1sCH2wXl8B0wdjsj8JY1hF1OhuugwhpAHtGxqewed2kCXayE+ZJqSTV4jw==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "17.3.12", + "@angular/core": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz", - "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.13.tgz", + "integrity": "sha512-TzWcrkopyjFF+WeDr2cRe8CcHjU72KfYV3Sm2TkBkcXrkYX5sDjGWrBGrG3hRB4e4okqchrOCvm1MiTdy2vKMA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "17.3.12" + "@angular/core": "18.2.13" }, "peerDependenciesMeta": { "@angular/core": { @@ -1944,15 +2124,14 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz", - "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.13.tgz", + "integrity": "sha512-DBSh4AQwkiJDSiVvJATRmjxf6wyUs9pwQLgaFdSlfuTRO+sdb0J2z1r3BYm8t0IqdoyXzdZq2YCH43EmyvD71g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/core": "7.23.9", + "@babel/core": "7.25.2", "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^3.0.0", + "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", @@ -1965,30 +2144,29 @@ "ngcc": "bundles/ngcc/index.js" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "17.3.12", - "typescript": ">=5.2 <5.5" + "@angular/compiler": "18.2.13", + "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -2007,85 +2185,91 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, + "node_modules/@angular/compiler-cli/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@angular/compiler-cli/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@angular/core": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz", - "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.13.tgz", + "integrity": "sha512-8mbWHMgO95OuFV1Ejy4oKmbe9NOJ3WazQf/f7wks8Bck7pcihd0IKhlPBNjFllbF5o+04EYSwFhEtvEgjMDClA==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.14.0" - } - }, - "node_modules/@angular/elements": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-17.3.12.tgz", - "integrity": "sha512-rUfEaV+Ol0bxtcEfNuf/7aVe+3/hAVJMNF/DHG71BSekCxPSH5WR6wE0zsXmVoTBadj+TUDlsyju9o9n3+C5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.12", - "rxjs": "^6.5.3 || ^7.4.0" + "zone.js": "~0.14.10" } }, "node_modules/@angular/forms": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz", - "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.13.tgz", + "integrity": "sha512-A67D867fu3DSBhdLWWZl/F5pr7v2+dRM2u3U7ZJ0ewh4a+sv+0yqWdJW+a8xIoiHxS+btGEJL2qAKJiH+MCFfg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.12", - "@angular/core": "17.3.12", - "@angular/platform-browser": "17.3.12", + "@angular/common": "18.2.13", + "@angular/core": "18.2.13", + "@angular/platform-browser": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz", - "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.13.tgz", + "integrity": "sha512-tu7ZzY6qD3ATdWFzcTcsAKe7M6cJeWbT/4/bF9unyGO3XBPcNYDKoiz10+7ap2PUd0fmPwvuvTvSNJiFEBnB8Q==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "17.3.12", - "@angular/common": "17.3.12", - "@angular/core": "17.3.12" + "@angular/animations": "18.2.13", + "@angular/common": "18.2.13", + "@angular/core": "18.2.13" }, "peerDependenciesMeta": { "@angular/animations": { @@ -2094,38 +2278,36 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.12.tgz", - "integrity": "sha512-DQwV7B2x/DRLRDSisngZRdLqHdYbbrqZv2Hmu4ZbnNYaWPC8qvzgE/0CvY+UkDat3nCcsfwsMnlDeB6TL7/IaA==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.13.tgz", + "integrity": "sha512-kbQCf9+8EpuJC7buBxhSiwBtXvjAwAKh6MznD6zd2pyCYqfY6gfRCZQRtK59IfgVtKmEONWI9grEyNIRoTmqJg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.12", - "@angular/compiler": "17.3.12", - "@angular/core": "17.3.12", - "@angular/platform-browser": "17.3.12" + "@angular/common": "18.2.13", + "@angular/compiler": "18.2.13", + "@angular/core": "18.2.13", + "@angular/platform-browser": "18.2.13" } }, "node_modules/@angular/router": { - "version": "17.3.12", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.12.tgz", - "integrity": "sha512-dg7PHBSW9fmPKTVzwvHEeHZPZdpnUqW/U7kj8D29HTP9ur8zZnx9QcnbplwPeYb8yYa62JMnZSEel2X4PxdYBg==", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.13.tgz", + "integrity": "sha512-VKmfgi/r/CkyBq9nChQ/ptmfu0JT/8ONnLVJ5H+SkFLRYJcIRyHLKjRihMCyVm6xM5yktOdCaW73NTQrFz7+bg==", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "17.3.12", - "@angular/core": "17.3.12", - "@angular/platform-browser": "17.3.12", + "@angular/common": "18.2.13", + "@angular/core": "18.2.13", + "@angular/platform-browser": "18.2.13", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -2226,15 +2408,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -2242,13 +2423,12 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2428,19 +2608,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", @@ -2499,9 +2666,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2584,13 +2751,12 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3097,16 +3263,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", + "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-remap-async-to-generator": "^7.25.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -3116,15 +3281,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3781,17 +3945,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", - "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz", + "integrity": "sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.1", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "engines": { @@ -3806,7 +3969,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -4091,33 +4253,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -4144,11 +4279,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", - "dev": true, - "license": "MIT", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -4696,33 +4829,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@compodoc/compodoc/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@compodoc/compodoc/node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", - "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@compodoc/compodoc/node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -4816,26 +4922,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@compodoc/compodoc/node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "node_modules/@compodoc/compodoc/node_modules/jackspeak": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", @@ -4865,13 +4951,6 @@ "node": ">=6" } }, - "node_modules/@compodoc/compodoc/node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@compodoc/compodoc/node_modules/lru-cache": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", @@ -4882,16 +4961,6 @@ "node": "20 || >=22" } }, - "node_modules/@compodoc/compodoc/node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, "node_modules/@compodoc/compodoc/node_modules/minimatch": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", @@ -4925,32 +4994,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@compodoc/compodoc/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@compodoc/compodoc/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@compodoc/compodoc/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -5095,16 +5138,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@electron/asar": { "version": "3.2.15", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.15.tgz", @@ -5541,395 +5574,123 @@ "node": "*" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", - "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", - "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", - "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", - "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" } }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", - "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", - "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", - "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", - "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", - "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", - "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", - "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", - "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", - "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", - "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", - "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", - "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", - "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", - "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", - "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", - "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", - "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", - "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", - "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -6083,9 +5844,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { @@ -6102,6 +5863,50 @@ "lit": "^2.1.3" } }, + "node_modules/@figspec/components/node_modules/@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "node_modules/@figspec/components/node_modules/lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/@figspec/components/node_modules/lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/@figspec/components/node_modules/lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/@figspec/react": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.3.tgz", @@ -6147,14 +5952,14 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -6208,6 +6013,252 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/checkbox": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@inquirer/editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", + "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6317,6 +6368,7 @@ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -6334,6 +6386,7 @@ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -6348,6 +6401,7 @@ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -6361,6 +6415,7 @@ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -6377,6 +6432,7 @@ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -6960,6 +7016,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", + "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", + "dev": true, + "dependencies": { + "@inquirer/type": "^1.5.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 6" + } + }, "node_modules/@lit-labs/react": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.2.1.tgz", @@ -6971,31 +7042,94 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", - "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", - "dev": true, + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.0.0" + "@lit-labs/ssr-dom-shim": "^1.2.0" } }, - "node_modules/@ljharb/through": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", - "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.13.tgz", + "integrity": "sha512-uiKPB0Fv6WEEOZjruu9a6wnW/8jrjzlZbxXscMB8kuCJ1k6kHpcBnuvaAWcqhbI7rqX5GKziwWEdD+wi2gNLfA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - } + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.13.tgz", + "integrity": "sha512-bEVIIfK5mSQoG1R19qA+fJOvCB+0wVGGnXHT3smchBVahYBdlPn2OsZZKzlHWfb1E+PhLBmYfqB5zQXFP7hJig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.13.tgz", + "integrity": "sha512-Yml1KlMzOnXj/tnW7yX8U78iAzTk39aILYvCPbqeewAq1kSzl+w59k/fiVkTBfvDi/oW/5YRxL+Fq+Y1Fr1r2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.13.tgz", + "integrity": "sha512-afbVrsMgZ9dUTNUchFpj5VkmJRxvht/u335jUJ7o23YTbNbnpmXif3VKQGCtnjSh+CZaqm6N3CPG8KO3zwyZ1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.13.tgz", + "integrity": "sha512-vOtxu0xC0SLdQ2WRXg8Qgd8T32ak4SPqk5zjItRszrJk2BdeXqfGxBJbP7o4aOvSPSmSSv46Lr1EP4HXU8v7Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.13.tgz", + "integrity": "sha512-UCrMJQY/gJnOl3XgbWRZZUvGGBuKy6i0YNSptgMzHBjs+QYDYR1Mt/RLTOPy4fzzves65O1EDmlL//OzEqoLlA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", @@ -7138,6 +7272,84 @@ "node": ">= 10" } }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@napi-rs/cli": { "version": "2.18.4", "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.18.4.tgz", @@ -7156,37 +7368,35 @@ } }, "node_modules/@ng-select/ng-select": { - "version": "12.0.7", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-12.0.7.tgz", - "integrity": "sha512-Eht1zlLP0DJxiXcKnq3aY/EJ8odomgU0hM0BJoPY6oX3XFHndtFtdPxlZfhVtQn+FwyDEh7306rRx6digxVssA==", - "license": "MIT", + "version": "13.9.1", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-13.9.1.tgz", + "integrity": "sha512-+DzQkQp8coGWZREflJM/qx7BXipV6HEVpZCXoa6fJJRHJfmUMsxa5uV6kUVmClUE98Rkffk9CPHt6kZcj8PuqQ==", "dependencies": { "tslib": "^2.3.1" }, "engines": { - "node": ">= 16", + "node": ">= 18", "npm": ">= 8" }, "peerDependencies": { - "@angular/common": "^17.0.0-rc.0", - "@angular/core": "^17.0.0-rc.0", - "@angular/forms": "^17.0.0-rc.0" + "@angular/common": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/forms": "^18.0.0" } }, "node_modules/@ngtools/webpack": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.11.tgz", - "integrity": "sha512-SfTCbplt4y6ak5cf2IfqdoVOsnoNdh/j6Vu+wb8WWABKwZ5yfr2S/Gk6ithSKcdIZhAF8DNBOoyk1EJuf8Xkfg==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.12.tgz", + "integrity": "sha512-FFJAwtWbtpncMOVNuULPBwFJB7GSjiUwO93eGTzRp8O4EPQ8lCQeFbezQm/NP34+T0+GBLGzPSuQT+muob8YKw==", "dev": true, - "license": "MIT", "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "typescript": ">=5.2 <5.5", + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <5.6", "webpack": "^5.54.0" } }, @@ -7228,12 +7438,21 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@npmcli/agent": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", "dev": true, - "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", @@ -7250,7 +7469,6 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.3.4" }, @@ -7263,7 +7481,6 @@ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -7276,15 +7493,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", @@ -7313,7 +7528,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^7.0.0", "ini": "^4.1.3", @@ -7329,22 +7543,11 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/@npmcli/git/node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/@npmcli/git/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "license": "ISC", "engines": { "node": ">=16" } @@ -7353,15 +7556,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/@npmcli/git/node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -7371,7 +7572,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -7387,7 +7587,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", "dev": true, - "license": "ISC", "dependencies": { "npm-bundled": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" @@ -7482,7 +7681,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -7492,7 +7690,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/git": "^5.0.0", "glob": "^10.2.2", @@ -7511,7 +7708,6 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" }, @@ -7523,15 +7719,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/@npmcli/package-json/node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -7541,7 +7735,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", "dev": true, - "license": "ISC", "dependencies": { "which": "^4.0.0" }, @@ -7554,7 +7747,6 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "license": "ISC", "engines": { "node": ">=16" } @@ -7564,7 +7756,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -7576,26 +7767,25 @@ } }, "node_modules/@npmcli/redact": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", - "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", "dev": true, - "license": "ISC", "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@npmcli/run-script": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", - "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/node-gyp": "^3.0.0", "@npmcli/package-json": "^5.0.0", "@npmcli/promise-spawn": "^7.0.0", "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", "which": "^4.0.0" }, "engines": { @@ -7607,17 +7797,24 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "license": "ISC", "engines": { "node": ">=16" } }, + "node_modules/@npmcli/run-script/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@npmcli/run-script/node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -7665,27 +7862,6 @@ "@parcel/watcher-win32-x64": "2.5.0" } }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", - "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher-darwin-arm64": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", @@ -7707,237 +7883,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", - "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", - "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", - "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", - "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", - "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", - "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", - "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", - "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", - "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", - "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", - "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@parcel/watcher/node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -7980,34 +7925,6 @@ "node": ">=14" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz", - "integrity": "sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz", - "integrity": "sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz", @@ -8022,296 +7939,29 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz", - "integrity": "sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==", - "cpu": [ - "x64" - ], + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz", - "integrity": "sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz", - "integrity": "sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz", - "integrity": "sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz", - "integrity": "sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz", - "integrity": "sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz", - "integrity": "sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz", - "integrity": "sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz", - "integrity": "sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz", - "integrity": "sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz", - "integrity": "sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz", - "integrity": "sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz", - "integrity": "sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz", - "integrity": "sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz", - "integrity": "sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@schematics/angular": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.11.tgz", - "integrity": "sha512-tvJpTgYC+hCnTyLszYRUZVyNTpPd+C44gh5CPTcG3qkqStzXQwynQAf6X/DjtwXbUiPQF0XfF0+0R489GpdZPA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.12.tgz", + "integrity": "sha512-sIoeipsisK5eTLW3XuNZYcal83AfslBbgI7LnV+3VrXwpasKPGHwo2ZdwhCd2IXAkuJ02Iyu7MyV0aQRM9i/3g==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.11", - "@angular-devkit/schematics": "17.3.11", - "jsonc-parser": "3.2.1" + "@angular-devkit/core": "18.2.12", + "@angular-devkit/schematics": "18.2.12", + "jsonc-parser": "3.3.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@schematics/angular/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@schematics/angular/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -8341,7 +7991,6 @@ "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.3.2" }, @@ -8354,7 +8003,6 @@ "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^16.14.0 || >=18.0.0" } @@ -8364,7 +8012,6 @@ "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^16.14.0 || >=18.0.0" } @@ -8374,7 +8021,6 @@ "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^2.3.2", "@sigstore/core": "^1.0.0", @@ -8392,7 +8038,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, - "license": "ISC", "dependencies": { "semver": "^7.3.5" }, @@ -8405,7 +8050,6 @@ "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -8429,7 +8073,6 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -8441,15 +8084,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/@sigstore/sign/node_modules/make-fetch-happen": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -8473,7 +8114,6 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -8486,7 +8126,6 @@ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, - "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", @@ -8504,7 +8143,6 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -8514,7 +8152,6 @@ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -8527,7 +8164,6 @@ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, - "license": "ISC", "dependencies": { "unique-slug": "^4.0.0" }, @@ -8540,7 +8176,6 @@ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, @@ -8553,7 +8188,6 @@ "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@sigstore/protobuf-specs": "^0.3.2", "tuf-js": "^2.2.1" @@ -8567,7 +8201,6 @@ "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^2.3.2", "@sigstore/core": "^1.1.0", @@ -9180,19 +8813,6 @@ "storybook": "^8.4.7" } }, - "node_modules/@storybook/core/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@storybook/csf": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.11.tgz", @@ -9341,6 +8961,56 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, + "node_modules/@storybook/web-components": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/web-components/-/web-components-8.4.7.tgz", + "integrity": "sha512-zR/bUWGkS5uxvqfXnW082ScrC4y5UrTdE1VKasezLGi5bTLub2hz8JP87PJgtWrq+mdrdmkLGzv5O4iJ/tlMAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/components": "8.4.7", + "@storybook/global": "^5.0.0", + "@storybook/manager-api": "8.4.7", + "@storybook/preview-api": "8.4.7", + "@storybook/theming": "8.4.7", + "tiny-invariant": "^1.3.1", + "ts-dedent": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "lit": "^2.0.0 || ^3.0.0", + "storybook": "^8.4.7" + } + }, + "node_modules/@storybook/web-components-webpack5": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/web-components-webpack5/-/web-components-webpack5-8.4.7.tgz", + "integrity": "sha512-RgLFQB7F4FOX5nOK3byaCo5Gs8nKMq1uNswOXdHSgZKfJfaZxmyMMGmnVUmOOLECsxyREokHwRDKma8SgFrRRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/builder-webpack5": "8.4.7", + "@storybook/web-components": "8.4.7", + "@types/node": "^22.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "lit": "^2.0.0 || ^3.0.0", + "storybook": "^8.4.7" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -9479,7 +9149,6 @@ "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, - "license": "MIT", "engines": { "node": "^16.14.0 || >=18.0.0" } @@ -9489,7 +9158,6 @@ "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", "dev": true, - "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^9.0.4" @@ -10071,10 +9739,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10148,7 +9825,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true, "license": "MIT" }, "node_modules/@types/plist": { @@ -10317,7 +9993,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -10349,6 +10024,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", @@ -10395,88 +10076,33 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", - "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", + "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/type-utils": "7.16.1", - "@typescript-eslint/utils": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/type-utils": "8.20.0", + "@typescript-eslint/utils": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", - "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", - "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/experimental-utils": { @@ -10629,46 +10255,42 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", - "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", + "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", - "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", + "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1" + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -10676,102 +10298,37 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", - "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", + "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.11.0", - "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/utils": "8.20.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", - "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", - "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", - "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.11.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", - "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", + "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -10779,154 +10336,87 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", - "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", - "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", + "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/typescript-estree": "7.11.0" + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", - "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "node_modules/@typescript-eslint/utils": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", - "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", - "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", - "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.11.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", - "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", + "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.20.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -11202,45 +10692,45 @@ "license": "BSD-3-Clause" }, "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "dev": true, "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { @@ -11389,19 +10879,6 @@ "node": ">=10" } }, - "node_modules/@yao-pkg/pkg/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -12065,6 +11542,7 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -12413,13 +11891,12 @@ } }, "node_modules/axobject-query": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", - "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" + "engines": { + "node": ">= 0.4" } }, "node_modules/babel-jest": { @@ -12469,6 +11946,7 @@ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -12497,6 +11975,46 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -12523,61 +12041,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", - "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -13107,9 +12589,9 @@ "license": "MIT" }, "node_modules/bufferutil": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", - "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -13481,7 +12963,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13504,6 +12985,7 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -13706,9 +13188,9 @@ } }, "node_modules/chromatic": { - "version": "11.20.0", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.20.0.tgz", - "integrity": "sha512-Btdli1qoAI01UKmk3Iqe6vKhAhePRXqNI/2uKKy2R16q7SN/5kLTqhd1JI20LFOZSnH3xSJaUXeJ2xZOJB//3A==", + "version": "11.20.2", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.20.2.tgz", + "integrity": "sha512-c+M3HVl5Y60c7ipGTZTyeWzWubRW70YsJ7PPDpO1D735ib8+Lu3yGF90j61pvgkXGngpkTPHZyBw83lcu2JMxA==", "dev": true, "license": "MIT", "bin": { @@ -14213,9 +13695,9 @@ } }, "node_modules/concurrently": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", - "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14383,7 +13865,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -14741,11 +14222,11 @@ "license": "MIT" }, "node_modules/critters": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", - "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", + "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", + "deprecated": "Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties", "dev": true, - "license": "Apache-2.0", "dependencies": { "chalk": "^4.1.0", "css-select": "^5.1.0", @@ -14756,26 +14237,6 @@ "postcss-media-query-parser": "^0.2.3" } }, - "node_modules/critters/node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -14924,7 +14385,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -15011,9 +14471,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -15745,9 +15205,9 @@ } }, "node_modules/electron": { - "version": "33.2.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz", - "integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==", + "version": "33.3.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.3.1.tgz", + "integrity": "sha512-Z7l2bVgpdKxHQMI4i0CirBX2n+iCYKOx5mbzNM3BpOyFELwlobEXKmzCmEnwP+3EcNeIhUQyIEBFQxN06QgdIw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -15972,23 +15432,10 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/electron-updater/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", - "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", + "version": "20.17.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", + "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", "dev": true, "license": "MIT", "dependencies": { @@ -16151,7 +15598,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -16318,42 +15764,42 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", - "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.1", - "@esbuild/android-arm": "0.20.1", - "@esbuild/android-arm64": "0.20.1", - "@esbuild/android-x64": "0.20.1", - "@esbuild/darwin-arm64": "0.20.1", - "@esbuild/darwin-x64": "0.20.1", - "@esbuild/freebsd-arm64": "0.20.1", - "@esbuild/freebsd-x64": "0.20.1", - "@esbuild/linux-arm": "0.20.1", - "@esbuild/linux-arm64": "0.20.1", - "@esbuild/linux-ia32": "0.20.1", - "@esbuild/linux-loong64": "0.20.1", - "@esbuild/linux-mips64el": "0.20.1", - "@esbuild/linux-ppc64": "0.20.1", - "@esbuild/linux-riscv64": "0.20.1", - "@esbuild/linux-s390x": "0.20.1", - "@esbuild/linux-x64": "0.20.1", - "@esbuild/netbsd-x64": "0.20.1", - "@esbuild/openbsd-x64": "0.20.1", - "@esbuild/sunos-x64": "0.20.1", - "@esbuild/win32-arm64": "0.20.1", - "@esbuild/win32-ia32": "0.20.1", - "@esbuild/win32-x64": "0.20.1" + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" } }, "node_modules/esbuild-register": { @@ -16370,16 +15816,15 @@ } }, "node_modules/esbuild-wasm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", - "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.23.0.tgz", + "integrity": "sha512-6jP8UmWy6R6TUUV8bMuC3ZyZ6lZKI56x0tkxyCIqWwRRJ/DgeQKneh/Oid5EoGoPFLrGNkz47ZEtWAYuiY/u9g==", "dev": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/escalade": { @@ -16401,7 +15846,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -16444,9 +15888,9 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", @@ -16454,8 +15898,8 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -16501,13 +15945,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", "dev": true, "license": "MIT", "bin": { - "eslint-config-prettier": "bin/cli.js" + "eslint-config-prettier": "build/bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -16552,19 +15996,20 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", - "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", "dev": true, "license": "ISC", "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -16574,7 +16019,16 @@ }, "peerDependencies": { "eslint": "*", - "eslint-plugin-import": "*" + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, "node_modules/eslint-module-utils": { @@ -16606,35 +16060,37 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { @@ -16735,167 +16191,27 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.8.0.tgz", - "integrity": "sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA==", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.11.2.tgz", + "integrity": "sha512-0Z4DUklJrC+GHjCRXa7PYfPzWC15DaVnwaOYenpgXiCEijXPZkLKCms+rHhtoRcWccP7Z8DpOOaP1gc3P9oOwg==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf": "^0.0.1", - "@typescript-eslint/utils": "^5.62.0", - "requireindex": "^1.2.0", + "@storybook/csf": "^0.1.11", + "@typescript-eslint/utils": "^8.8.1", "ts-dedent": "^2.2.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "eslint": ">=6" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@storybook/csf": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.1.tgz", - "integrity": "sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-plugin-storybook/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" + "eslint": ">=8" } }, "node_modules/eslint-plugin-tailwindcss": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.4.tgz", - "integrity": "sha512-gJAEHmCq2XFfUP/+vwEfEJ9igrPeZFg+skeMtsxquSQdxba9XRk5bn0Bp9jxG1VV9/wwPKi1g3ZjItu6MIjhNg==", + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.5.tgz", + "integrity": "sha512-8Mi7p7dm+mO1dHgRHHFdPu4RDTBk69Cn4P0B40vRQR+MrguUpwmKwhZy1kqYe3Km8/4nb+cyrCF+5SodOEmaow==", "dev": true, "license": "MIT", "dependencies": { @@ -17314,9 +16630,9 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", "dependencies": { @@ -17339,7 +16655,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -17354,6 +16670,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -17406,9 +16726,9 @@ } }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, @@ -17829,6 +17149,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -18158,9 +17484,9 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -18455,6 +17781,7 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -19060,6 +18387,25 @@ "node": ">=12" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", @@ -19295,9 +18641,9 @@ } }, "node_modules/husky": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.4.tgz", - "integrity": "sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==", + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -19430,7 +18776,6 @@ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", "dev": true, - "license": "ISC", "dependencies": { "minimatch": "^9.0.0" }, @@ -19462,14 +18807,12 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -19486,7 +18829,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -19635,11 +18977,10 @@ "license": "ISC" }, "node_modules/ini": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", - "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -19691,6 +19032,16 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/into-stream": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", @@ -19777,7 +19128,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-bigint": { @@ -19823,6 +19173,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -19853,7 +19213,6 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -20105,6 +19464,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -20306,6 +19674,7 @@ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -20323,6 +19692,7 @@ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" } @@ -21089,28 +20459,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-extended": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", - "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-diff": "^29.0.0", - "jest-get-type": "^29.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "jest": ">=27.2.5" - }, - "peerDependenciesMeta": { - "jest": { - "optional": true - } - } - }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -21148,6 +20496,16 @@ "fsevents": "^2.3.2" } }, + "node_modules/jest-haste-map/node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/jest-junit": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", @@ -21928,6 +21286,7 @@ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -22055,7 +21414,6 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, - "license": "MIT", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -22118,11 +21476,10 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true, - "license": "MIT" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true }, "node_modules/jsonfile": { "version": "6.1.0", @@ -22152,8 +21509,7 @@ "dev": true, "engines": [ "node >= 0.2.0" - ], - "license": "MIT" + ] }, "node_modules/jsqr": { "version": "1.4.0", @@ -22272,16 +21628,6 @@ "node": ">=6" } }, - "node_modules/klona": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", - "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/koa": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", @@ -22512,24 +21858,29 @@ } }, "node_modules/less-loader": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", - "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", "dev": true, - "license": "MIT", - "dependencies": { - "klona": "^2.0.4" - }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "less": "^3.5.0 || ^4.0.0", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/less/node_modules/make-dir": { @@ -22663,26 +22014,25 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.2.8", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.8.tgz", - "integrity": "sha512-PUWFf2zQzsd9EFU+kM1d7UP+AZDbKFKuj+9JNVTBkhUFhbg4MAt6WfyMMwBfM4lYqd4D2Jwac5iuTu9rVj4zCQ==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.1.tgz", + "integrity": "sha512-P8yJuVRyLrm5KxCtFx+gjI5Bil+wO7wnTl7C3bXhvtTaAFGirzeB24++D0wGoUwxrUKecNiehemgCob9YL39NA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "~5.3.0", + "chalk": "~5.4.1", "commander": "~12.1.0", - "debug": "~4.3.6", + "debug": "~4.4.0", "execa": "~8.0.1", - "lilconfig": "~3.1.2", - "listr2": "~8.2.4", - "micromatch": "~4.0.7", + "lilconfig": "~3.1.3", + "listr2": "~8.2.5", + "micromatch": "~4.0.8", "pidtree": "~0.6.0", "string-argv": "~0.3.2", - "yaml": "~2.5.0" + "yaml": "~2.6.1" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -22695,9 +22045,9 @@ } }, "node_modules/lint-staged/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, "license": "MIT", "engines": { @@ -23006,39 +22356,67 @@ } }, "node_modules/lit": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", - "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz", + "integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==", "license": "BSD-3-Clause", "dependencies": { - "@lit/reactive-element": "^1.6.0", - "lit-element": "^3.3.0", - "lit-html": "^2.8.0" + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.1.0", + "lit-html": "^3.2.0" } }, "node_modules/lit-element": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", - "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz", + "integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==", "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.0", - "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.8.0" + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.2.0" } }, "node_modules/lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz", + "integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==", "license": "BSD-3-Clause", "dependencies": { "@types/trusted-types": "^2.0.2" } }, + "node_modules/lmdb": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.13.tgz", + "integrity": "sha512-UGe+BbaSUQtAMZobTb4nHvFMrmvuAQKSeaqAX2meTEQjfsbpl5sxdHD8T72OnwD4GU9uwNhYXIVe4QGs8N9Zyw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "msgpackr": "^1.10.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.0.13", + "@lmdb/lmdb-darwin-x64": "3.0.13", + "@lmdb/lmdb-linux-arm": "3.0.13", + "@lmdb/lmdb-linux-arm64": "3.0.13", + "@lmdb/lmdb-linux-x64": "3.0.13", + "@lmdb/lmdb-win32-x64": "3.0.13" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -23050,11 +22428,10 @@ } }, "node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 12.13.0" } @@ -23502,16 +22879,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-dir": { @@ -24673,8 +24046,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/minimatch": { "version": "9.0.5", @@ -24815,37 +24187,6 @@ "dev": true, "license": "ISC" }, - "node_modules/minipass-json-stream": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz", - "integrity": "sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-json-stream/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", @@ -25039,6 +24380,37 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, "node_modules/multer": { "version": "1.4.5-lts.1", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", @@ -25204,16 +24576,15 @@ } }, "node_modules/ngx-infinite-scroll": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-17.0.1.tgz", - "integrity": "sha512-T+XseajbmT9YTMmPnFV/AfSlwjaV9m2gZtbIIZH3S+yg/rvvfbgkThqs54UWIu+pqcqNR4UhrXfw6mUjCVZD2A==", - "license": "MIT", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-18.0.0.tgz", + "integrity": "sha512-D183TDwpsd9Zl56UmItsl3RzHdN25srAISfg6lc3A8mEKkEgOq0s7ZzRAYcx8DHsAkMgtZqjIPEvMifD3DOB/g==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": ">=17.0.0 <18.0.0", - "@angular/core": ">=17.0.0 <18.0.0" + "@angular/common": ">=18.0.0 <19.0.0", + "@angular/core": ">=18.0.0 <19.0.0" } }, "node_modules/ngx-toastr": { @@ -25236,7 +24607,6 @@ "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "!win32" @@ -25251,7 +24621,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/no-case": { @@ -25360,7 +24729,6 @@ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", "dev": true, - "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", @@ -25391,12 +24759,25 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-gyp/node_modules/@npmcli/fs": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, - "license": "ISC", "dependencies": { "semver": "^7.3.5" }, @@ -25409,7 +24790,6 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -25419,7 +24799,6 @@ "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -25443,7 +24822,6 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -25456,7 +24834,6 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "license": "ISC", "engines": { "node": ">=16" } @@ -25465,15 +24842,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/node-gyp/node_modules/make-fetch-happen": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -25497,7 +24872,6 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -25510,7 +24884,6 @@ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, - "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", @@ -25528,7 +24901,6 @@ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, - "license": "ISC", "dependencies": { "abbrev": "^2.0.0" }, @@ -25544,7 +24916,6 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -25554,7 +24925,6 @@ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -25567,7 +24937,6 @@ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, - "license": "ISC", "dependencies": { "unique-slug": "^4.0.0" }, @@ -25580,7 +24949,6 @@ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, @@ -25593,7 +24961,6 @@ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dev": true, - "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -25660,7 +25027,6 @@ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", @@ -25675,7 +25041,6 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" }, @@ -25687,8 +25052,7 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/normalize-path": { "version": "3.0.0", @@ -25728,7 +25092,6 @@ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", "dev": true, - "license": "ISC", "dependencies": { "npm-normalize-package-bin": "^3.0.0" }, @@ -25741,7 +25104,6 @@ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, @@ -25754,20 +25116,18 @@ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm-package-arg": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", - "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", "dev": true, - "license": "ISC", "dependencies": { "hosted-git-info": "^7.0.0", - "proc-log": "^3.0.0", + "proc-log": "^4.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" }, @@ -25780,7 +25140,6 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" }, @@ -25792,15 +25151,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/npm-package-arg/node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -25810,7 +25167,6 @@ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", "dev": true, - "license": "ISC", "dependencies": { "ignore-walk": "^6.0.4" }, @@ -25819,11 +25175,10 @@ } }, "node_modules/npm-pick-manifest": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", - "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", "dev": true, - "license": "ISC", "dependencies": { "npm-install-checks": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", @@ -25835,17 +25190,16 @@ } }, "node_modules/npm-registry-fetch": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", - "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", "dev": true, - "license": "ISC", "dependencies": { - "@npmcli/redact": "^1.1.0", + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", "make-fetch-happen": "^13.0.0", "minipass": "^7.0.2", "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", "minizlib": "^2.1.2", "npm-package-arg": "^11.0.0", "proc-log": "^4.0.0" @@ -25859,7 +25213,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, - "license": "ISC", "dependencies": { "semver": "^7.3.5" }, @@ -25872,7 +25225,6 @@ "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -25896,7 +25248,6 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -25908,15 +25259,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -25940,7 +25289,6 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -25953,7 +25301,6 @@ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, - "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", @@ -25971,7 +25318,6 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -25981,7 +25327,6 @@ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -25994,7 +25339,6 @@ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, - "license": "ISC", "dependencies": { "unique-slug": "^4.0.0" }, @@ -26007,7 +25351,6 @@ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, @@ -26306,6 +25649,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true + }, "node_modules/os-name": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.1.tgz", @@ -26453,33 +25802,31 @@ "license": "BlueOak-1.0.0" }, "node_modules/pacote": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.6.tgz", - "integrity": "sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ==", + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/git": "^5.0.0", "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^7.0.0", + "@npmcli/run-script": "^8.0.0", "cacache": "^18.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^11.0.0", "npm-packlist": "^8.0.0", "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^16.0.0", - "proc-log": "^3.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", "promise-retry": "^2.0.1", - "read-package-json": "^7.0.0", - "read-package-json-fast": "^3.0.0", "sigstore": "^2.2.0", "ssri": "^10.0.0", "tar": "^6.1.11" }, "bin": { - "pacote": "lib/bin.js" + "pacote": "bin/index.js" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -26490,7 +25837,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, - "license": "ISC", "dependencies": { "semver": "^7.3.5" }, @@ -26503,7 +25849,6 @@ "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -26527,7 +25872,6 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -26539,15 +25883,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/pacote/node_modules/minipass-collect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -26556,11 +25898,10 @@ } }, "node_modules/pacote/node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -26570,7 +25911,6 @@ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -26583,7 +25923,6 @@ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, - "license": "ISC", "dependencies": { "unique-slug": "^4.0.0" }, @@ -26596,7 +25935,6 @@ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, @@ -26631,7 +25969,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -26644,7 +25981,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -26663,7 +25999,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/parse-node-version": { @@ -26927,7 +26262,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -26964,7 +26298,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27007,11 +26340,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -27052,11 +26384,10 @@ } }, "node_modules/piscina": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", - "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", + "integrity": "sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==", "dev": true, - "license": "MIT", "optionalDependencies": { "nice-napi": "^1.0.2" } @@ -27432,8 +26763,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/postcss-modules-extract-imports": { "version": "3.1.0", @@ -28168,37 +27498,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/read-package-json": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", - "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", - "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -28291,6 +27590,19 @@ "node": ">=0.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -28336,7 +27648,6 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { @@ -28635,7 +27946,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -29184,9 +28494,9 @@ } }, "node_modules/sass": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", - "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", + "version": "1.83.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", + "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", "dev": true, "license": "MIT", "dependencies": { @@ -29246,9 +28556,9 @@ } }, "node_modules/sass/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { @@ -29269,13 +28579,13 @@ "license": "MIT" }, "node_modules/sass/node_modules/readdirp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", - "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": ">= 14.18.0" }, "funding": { "type": "individual", @@ -29371,13 +28681,10 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -29393,24 +28700,6 @@ "license": "MIT", "optional": true }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, "node_modules/send": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", @@ -29784,7 +29073,6 @@ "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@sigstore/bundle": "^2.3.2", "@sigstore/core": "^1.0.0", @@ -30020,7 +29308,6 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -30030,15 +29317,13 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" + "dev": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -30048,8 +29333,7 @@ "version": "3.0.20", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", - "dev": true, - "license": "CC0-1.0" + "dev": true }, "node_modules/spdy": { "version": "4.0.2", @@ -30101,7 +29385,8 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/ssri": { "version": "9.0.1", @@ -30136,6 +29421,13 @@ "dev": true, "license": "ISC" }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -30535,6 +29827,12 @@ "webpack": "^5.27.0" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -30597,7 +29895,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -30643,9 +29940,9 @@ "license": "MIT" }, "node_modules/tailwindcss": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", - "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, "license": "MIT", "dependencies": { @@ -30812,11 +30109,10 @@ } }, "node_modules/terser": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", - "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -30962,6 +30258,7 @@ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -30977,6 +30274,7 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -30989,6 +30287,7 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -31010,6 +30309,7 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -31101,19 +30401,6 @@ "node": ">=12.0.0" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinyrainbow": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", @@ -31135,21 +30422,21 @@ } }, "node_modules/tldts": { - "version": "6.1.69", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.69.tgz", - "integrity": "sha512-Oh/CqRQ1NXNY7cy9NkTPUauOWiTro0jEYZTioGbOmcQh6EC45oribyIMJp0OJO3677r13tO6SKdWoGZUx2BDFw==", + "version": "6.1.71", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.71.tgz", + "integrity": "sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.69" + "tldts-core": "^6.1.71" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.69", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.69.tgz", - "integrity": "sha512-nygxy9n2PBUFQUtAXAc122gGo+04/j5qr5TGQFZTHafTKYvmARVXt2cA5rgero2/dnXUfkdPtiJoKmrd3T+wdA==", + "version": "6.1.71", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.71.tgz", + "integrity": "sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==", "license": "MIT" }, "node_modules/tmp": { @@ -31286,16 +30573,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", - "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-dedent": { @@ -31425,14 +30712,15 @@ } }, "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", - "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", "tsconfig-paths": "^4.1.2" }, "engines": { @@ -31488,10 +30776,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "license": "0BSD" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tsscmp": { "version": "1.0.6", @@ -31549,7 +30836,6 @@ "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", "dev": true, - "license": "MIT", "dependencies": { "@tufjs/models": "2.0.1", "debug": "^4.3.4", @@ -31564,7 +30850,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, - "license": "ISC", "dependencies": { "semver": "^7.3.5" }, @@ -31577,7 +30862,6 @@ "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -31601,7 +30885,6 @@ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -31613,15 +30896,13 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" + "dev": true }, "node_modules/tuf-js/node_modules/make-fetch-happen": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", "dev": true, - "license": "ISC", "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -31645,7 +30926,6 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -31658,7 +30938,6 @@ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, - "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", @@ -31676,7 +30955,6 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -31686,7 +30964,6 @@ "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -31699,7 +30976,6 @@ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", "dev": true, - "license": "ISC", "dependencies": { "unique-slug": "^4.0.0" }, @@ -31712,7 +30988,6 @@ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, @@ -32038,16 +31313,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", - "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0" - } - }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -32440,7 +31705,6 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, - "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -32451,7 +31715,6 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -32527,15 +31790,14 @@ } }, "node_modules/vite": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz", - "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, - "license": "MIT", "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -32554,6 +31816,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -32571,6 +31834,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -32582,83 +31848,14 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -32667,319 +31864,12 @@ "node": ">=12" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/vite/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -32987,29 +31877,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/w3c-xmlserializer": { @@ -33051,23 +31941,11 @@ "dev": true, "license": "ISC" }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, - "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -33081,7 +31959,6 @@ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, - "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } @@ -33090,16 +31967,20 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", "dependencies": { "defaults": "^1.0.3" } }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", "engines": { "node": ">=12" } @@ -33109,7 +31990,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, - "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -33152,43 +32032,39 @@ } }, "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, - "license": "MIT", "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", "colorette": "^2.0.14", - "commander": "^10.0.1", + "commander": "^12.1.0", "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", + "envinfo": "^7.14.0", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" + "webpack-merge": "^6.0.1" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "5.x.x" + "webpack": "^5.82.0" }, "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, "webpack-bundle-analyzer": { "optional": true }, @@ -33197,45 +32073,30 @@ } } }, + "node_modules/webpack-cli/node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-cli/node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-cli/node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" + "node": ">=18" } }, "node_modules/webpack-dev-middleware": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", - "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", + "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", "dev": true, - "license": "MIT", "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.12", @@ -33260,11 +32121,10 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", - "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", + "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", "dev": true, - "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -33279,23 +32139,20 @@ "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.4.0", - "http-proxy-middleware": "^2.0.3", + "http-proxy-middleware": "^2.0.7", "ipaddr.js": "^2.1.0", "launch-editor": "^2.6.1", "open": "^10.0.3", "p-retry": "^6.2.0", - "rimraf": "^5.0.5", "schema-utils": "^4.2.0", "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.1.0", - "ws": "^8.16.0" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" @@ -33324,7 +32181,6 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -33337,7 +32193,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10" } @@ -33347,7 +32202,6 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, - "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" }, @@ -33359,11 +32213,10 @@ } }, "node_modules/webpack-dev-server/node_modules/memfs": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.14.0.tgz", - "integrity": "sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==", + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.15.3.tgz", + "integrity": "sha512-vR/g1SgqvKJgAyYla+06G4p/EOcEmwhYuVb1yc1ixcKf8o/sh7Zngv63957ZSNd1xrZJoinmNyDf2LzuP8WJXw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/json-pack": "^1.0.3", "@jsonjoy.com/util": "^1.3.0", @@ -33397,22 +32250,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", @@ -33456,18 +32293,17 @@ } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, - "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-node-externals": { @@ -33524,7 +32360,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -33541,7 +32376,6 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } @@ -33598,7 +32432,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -33614,8 +32447,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", @@ -33983,9 +32815,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -34056,6 +32888,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zip-stream": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", diff --git a/package.json b/package.json index 03991f9d70b..48c2e03b129 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:watch:all": "jest --watchAll", "test:types": "node ./scripts/test-types.js", "test:locales": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/test-locales.js", + "lint:dep-ownership": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/dep-ownership.js", "docs:json": "compodoc -p ./tsconfig.json -e json -d . --disableRoutesGraph", "storybook": "ng run components:storybook", "build-storybook": "ng run components:build-storybook", @@ -35,20 +36,19 @@ "libs/*" ], "devDependencies": { - "@angular-devkit/build-angular": "17.3.11", - "@angular-eslint/eslint-plugin": "17.5.3", - "@angular-eslint/eslint-plugin-template": "17.5.3", - "@angular-eslint/schematics": "17.5.3", - "@angular-eslint/template-parser": "17.5.3", - "@angular/cli": "17.3.11", - "@angular/compiler-cli": "17.3.12", - "@angular/elements": "17.3.12", + "@angular-devkit/build-angular": "18.2.12", + "@angular-eslint/eslint-plugin": "18.4.3", + "@angular-eslint/eslint-plugin-template": "18.4.3", + "@angular-eslint/schematics": "18.4.3", + "@angular-eslint/template-parser": "18.4.3", + "@angular/cli": "18.2.12", + "@angular/compiler-cli": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", "@electron/notarize": "2.5.0", "@electron/rebuild": "3.7.1", - "@ngtools/webpack": "17.3.11", + "@ngtools/webpack": "18.2.12", "@storybook/addon-a11y": "8.4.7", "@storybook/addon-actions": "8.4.7", "@storybook/addon-designs": "8.0.4", @@ -58,6 +58,7 @@ "@storybook/angular": "8.4.7", "@storybook/manager-api": "8.4.7", "@storybook/theming": "8.4.7", + "@storybook/web-components-webpack5": "8.4.7", "@types/argon2-browser": "1.18.4", "@types/chrome": "0.0.280", "@types/firefox-webext-browser": "120.0.4", @@ -72,7 +73,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.1", + "@types/node": "22.10.5", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -80,42 +81,42 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", "babel-loader": "9.2.1", "base64-loader": "1.0.0", "browserslist": "4.23.2", - "chromatic": "11.20.0", - "concurrently": "9.1.0", + "chromatic": "11.20.2", + "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.2.1", + "electron": "33.3.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", "electron-store": "8.2.0", "electron-updater": "6.3.9", - "eslint": "8.57.0", - "eslint-config-prettier": "9.1.0", - "eslint-import-resolver-typescript": "3.6.1", - "eslint-plugin-import": "2.29.1", + "eslint": "8.57.1", + "eslint-config-prettier": "10.0.1", + "eslint-import-resolver-typescript": "3.7.0", + "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", - "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.17.4", + "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-tailwindcss": "3.17.5", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.3", - "husky": "9.1.4", - "jest-extended": "4.0.2", + "husky": "9.1.7", + "jest-diff": "29.7.0", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", - "lint-staged": "15.2.8", + "lint-staged": "15.4.1", "mini-css-extract-plugin": "2.9.2", "node-ipc": "9.2.1", "postcss": "8.4.49", @@ -125,14 +126,14 @@ "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.83.4", "sass-loader": "16.0.4", "storybook": "8.4.7", "style-loader": "4.0.0", - "tailwindcss": "3.4.16", + "tailwindcss": "3.4.17", "ts-jest": "29.2.2", "ts-loader": "9.5.1", - "tsconfig-paths-webpack-plugin": "4.1.0", + "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", "typescript-strict-plugin": "^2.4.4", @@ -140,38 +141,39 @@ "util": "0.12.5", "wait-on": "8.0.1", "webpack": "5.97.1", - "webpack-cli": "5.1.4", - "webpack-dev-server": "5.0.4", + "webpack-cli": "6.0.1", + "webpack-dev-server": "5.2.0", "webpack-node-externals": "3.0.0" }, "dependencies": { - "@angular/animations": "17.3.12", - "@angular/cdk": "17.3.10", - "@angular/common": "17.3.12", - "@angular/compiler": "17.3.12", - "@angular/core": "17.3.12", - "@angular/forms": "17.3.12", - "@angular/platform-browser": "17.3.12", - "@angular/platform-browser-dynamic": "17.3.12", - "@angular/router": "17.3.12", + "@angular/animations": "18.2.13", + "@angular/cdk": "18.2.14", + "@angular/common": "18.2.13", + "@angular/compiler": "18.2.13", + "@angular/core": "18.2.13", + "@angular/forms": "18.2.13", + "@angular/platform-browser": "18.2.13", + "@angular/platform-browser-dynamic": "18.2.13", + "@angular/router": "18.2.13", "@bitwarden/sdk-internal": "0.2.0-main.38", "@electron/fuses": "1.8.0", + "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7", - "@ng-select/ng-select": "12.0.7", + "@ng-select/ng-select": "13.9.1", "argon2": "0.41.1", "argon2-browser": "1.18.0", "big-integer": "1.6.52", "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", - "bufferutil": "4.0.8", + "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.39.0", - "form-data": "4.0.0", + "form-data": "4.0.1", "https-proxy-agent": "7.0.5", "inquirer": "8.2.6", "jquery": "3.7.1", @@ -180,10 +182,11 @@ "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", + "lit": "3.2.1", "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.1", - "ngx-infinite-scroll": "17.0.1", + "ngx-infinite-scroll": "18.0.0", "ngx-toastr": "19.0.0", "node-fetch": "2.6.12", "node-forge": "1.3.1", @@ -198,7 +201,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.69", + "tldts": "6.1.71", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" diff --git a/scripts/dep-ownership.ts b/scripts/dep-ownership.ts new file mode 100644 index 00000000000..e574a3e9e96 --- /dev/null +++ b/scripts/dep-ownership.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ + +/// Ensure that all dependencies in package.json have an owner in the renovate.json file. + +import fs from "fs"; +import path from "path"; + +const renovateConfig = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json"), "utf8"), +); + +const packagesWithOwners = renovateConfig.packageRules + .flatMap((rule: any) => rule.matchPackageNames) + .filter((packageName: string) => packageName != null); + +const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8"), +); +const dependencies = Object.keys(packageJson.dependencies).concat( + Object.keys(packageJson.devDependencies), +); + +const missingOwners = dependencies.filter((dep) => !packagesWithOwners.includes(dep)); + +if (missingOwners.length > 0) { + console.error("Missing owners for the following dependencies:"); + console.error(missingOwners.join("\n")); + process.exit(1); +} + +console.log("All dependencies have owners."); diff --git a/scripts/test-types.js b/scripts/test-types.js index 2155406cdf0..9534558af30 100644 --- a/scripts/test-types.js +++ b/scripts/test-types.js @@ -16,10 +16,15 @@ function getFiles(dir) { return results; } -const files = getFiles(path.join(__dirname, "..", "libs")).filter((file) => { - const name = path.basename(file); - return name === "tsconfig.spec.json"; -}); +const files = getFiles(path.join(__dirname, "..", "libs")) + .filter((file) => { + const name = path.basename(file); + return name === "tsconfig.spec.json"; + }) + .filter((path) => { + // Exclude shared since it's not actually a library + return !path.includes("libs/shared/"); + }); concurrently([ { diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 80f70e47b09..980d7832ace 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -14,11 +14,13 @@ "declarationDir": "dist/types", "outDir": "dist", "baseUrl": ".", + "allowJs": true, "paths": { "@bitwarden/admin-console": ["./libs/admin-console/src"], "@bitwarden/angular/*": ["./libs/angular/src/*"], "@bitwarden/auth": ["./libs/auth/src"], "@bitwarden/billing": ["./libs/billing/src"], + "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/generator-components": ["./libs/tools/generator/components/src"], @@ -26,16 +28,18 @@ "@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"], - "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], - "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], + "@bitwarden/key-management": ["./libs/key-management/src"], + "@bitwarden/key-management/angular": ["./libs/key-management/src/angular"], + "@bitwarden/node/*": ["./libs/node/src/*"], + "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": [".libs/tools/send/send-ui/src"], "@bitwarden/tools-card": [".libs/tools/card/src"], - "@bitwarden/platform": ["./libs/platform/src"], - "@bitwarden/node/*": ["./libs/node/src/*"], - "@bitwarden/vault": ["./libs/vault/src"], - "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"] + "@bitwarden/ui-common": ["./libs/ui/common/src"], + "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], + "@bitwarden/vault": ["./libs/vault/src"] }, "plugins": [ { diff --git a/tsconfig.json b/tsconfig.json index 47b561e0829..efa2ff70e1b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,9 +18,10 @@ "paths": { "@bitwarden/admin-console/common": ["./libs/admin-console/src/common"], "@bitwarden/angular/*": ["./libs/angular/src/*"], - "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/auth/angular": ["./libs/auth/src/angular"], + "@bitwarden/auth/common": ["./libs/auth/src/common"], "@bitwarden/billing": ["./libs/billing/src"], + "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"], "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/generator-components": ["./libs/tools/generator/components/src"], @@ -28,18 +29,19 @@ "@bitwarden/generator-history": ["./libs/tools/generator/extensions/history/src"], "@bitwarden/generator-legacy": ["./libs/tools/generator/extensions/legacy/src"], "@bitwarden/generator-navigation": ["./libs/tools/generator/extensions/navigation/src"], - "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], - "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/key-management": ["./libs/key-management/src"], + "@bitwarden/key-management/angular": ["./libs/key-management/src/angular"], + "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["./libs/tools/card/src"], - "@bitwarden/node/*": ["./libs/node/src/*"], - "@bitwarden/web-vault/*": ["./apps/web/src/*"], + "@bitwarden/ui-common": ["./libs/ui/common/src"], + "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/vault": ["./libs/vault/src"], - "@bitwarden/bit-common/*": ["./bitwarden_license/bit-common/src/*"] + "@bitwarden/web-vault/*": ["./apps/web/src/*"] }, "plugins": [ {