mirror of
https://github.com/bitwarden/clients.git
synced 2026-02-03 18:09:33 -06:00
Merge branch 'main' into desktop/add-folder
This commit is contained in:
commit
2c074da38a
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@ -15,6 +15,10 @@ apps/desktop/desktop_native/core/src/secure_memory @bitwarden/team-key-managemen
|
||||
apps/desktop/desktop_native/Cargo.lock
|
||||
apps/desktop/desktop_native/Cargo.toml
|
||||
|
||||
# Web connectors
|
||||
apps/web/src/connectors @bitwarden/team-auth-dev
|
||||
apps/web/src/connectors/platform @bitwarden/team-platform-dev
|
||||
|
||||
## Auth team files ##
|
||||
apps/browser/src/auth @bitwarden/team-auth-dev
|
||||
apps/cli/src/auth @bitwarden/team-auth-dev
|
||||
@ -22,8 +26,6 @@ apps/desktop/src/auth @bitwarden/team-auth-dev
|
||||
apps/web/src/app/auth @bitwarden/team-auth-dev
|
||||
libs/auth @bitwarden/team-auth-dev
|
||||
libs/user-core @bitwarden/team-auth-dev
|
||||
# web connectors used for auth
|
||||
apps/web/src/connectors @bitwarden/team-auth-dev
|
||||
bitwarden_license/bit-web/src/app/auth @bitwarden/team-auth-dev
|
||||
libs/angular/src/auth @bitwarden/team-auth-dev
|
||||
libs/common/src/auth @bitwarden/team-auth-dev
|
||||
@ -154,6 +156,9 @@ apps/desktop/macos/autofill-extension @bitwarden/team-autofill-desktop-dev
|
||||
apps/desktop/src/app/components/fido2placeholder.component.ts @bitwarden/team-autofill-desktop-dev
|
||||
apps/desktop/desktop_native/windows_plugin_authenticator @bitwarden/team-autofill-desktop-dev
|
||||
apps/desktop/desktop_native/autotype @bitwarden/team-autofill-desktop-dev
|
||||
apps/desktop/desktop_native/napi/src/autofill.rs @bitwarden/team-autofill-desktop-dev
|
||||
apps/desktop/desktop_native/napi/src/autotype.rs @bitwarden/team-autofill-desktop-dev
|
||||
apps/desktop/desktop_native/napi/src/sshagent.rs @bitwarden/team-autofill-desktop-dev
|
||||
# DuckDuckGo integration
|
||||
apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-desktop-dev
|
||||
apps/desktop/src/services/duckduckgo-message-handler.service.ts @bitwarden/team-autofill-desktop-dev
|
||||
|
||||
@ -73,7 +73,7 @@ jobs:
|
||||
_MONITORED_FILES: ${{ steps.changed-files.outputs.monitored_files }}
|
||||
with:
|
||||
script: |
|
||||
const changedFiles = `$_MONITORED_FILES`.split(' ').filter(file => file.trim() !== '');
|
||||
const changedFiles = process.env._MONITORED_FILES.split(' ').filter(file => file.trim() !== '');
|
||||
|
||||
const message = `<!-- comment_tag: ddg-test-warning -->
|
||||
⚠️🦆 **DuckDuckGo Integration files have been modified in this PR:**
|
||||
|
||||
26
.github/workflows/build-desktop.yml
vendored
26
.github/workflows/build-desktop.yml
vendored
@ -236,7 +236,7 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
@ -399,7 +399,7 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
@ -562,7 +562,7 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
@ -827,7 +827,7 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
@ -1032,14 +1032,14 @@ jobs:
|
||||
|
||||
- name: Cache Build
|
||||
id: build-cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: apps/desktop/build
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-build
|
||||
|
||||
- name: Cache Safari
|
||||
id: safari-cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: apps/browser/dist/Safari
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
|
||||
@ -1185,7 +1185,7 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
@ -1272,14 +1272,14 @@ jobs:
|
||||
|
||||
- name: Get Build Cache
|
||||
id: build-cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: apps/desktop/build
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-build
|
||||
|
||||
- name: Setup Safari Cache
|
||||
id: safari-cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: apps/browser/dist/Safari
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
|
||||
@ -1409,7 +1409,7 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
@ -1547,14 +1547,14 @@ jobs:
|
||||
|
||||
- name: Get Build Cache
|
||||
id: build-cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: apps/desktop/build
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-build
|
||||
|
||||
- name: Setup Safari Cache
|
||||
id: safari-cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: apps/browser/dist/Safari
|
||||
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
|
||||
@ -1692,7 +1692,7 @@ jobs:
|
||||
npm link ../sdk-internal
|
||||
|
||||
- name: Cache Native Module
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
id: cache
|
||||
with:
|
||||
path: |
|
||||
|
||||
18
.github/workflows/build-web.yml
vendored
18
.github/workflows/build-web.yml
vendored
@ -63,6 +63,11 @@ jobs:
|
||||
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Log inputs to job summary
|
||||
uses: bitwarden/ios/.github/actions/log-inputs@main
|
||||
with:
|
||||
inputs: "${{ toJson(inputs) }}"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
@ -181,6 +186,19 @@ jobs:
|
||||
ref: ${{ steps.set-server-ref.outputs.server_ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download SDK Artifacts
|
||||
if: ${{ inputs.sdk_branch != '' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@main
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workflow: build-wasm-internal.yml
|
||||
workflow_conclusion: success
|
||||
branch: ${{ inputs.sdk_branch }}
|
||||
artifacts: sdk-internal
|
||||
repo: bitwarden/sdk-internal
|
||||
path: sdk-internal
|
||||
if_no_artifact_found: fail
|
||||
|
||||
- name: Check Branch to Publish
|
||||
env:
|
||||
PUBLISH_BRANCHES: "main,rc,hotfix-rc-web"
|
||||
|
||||
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
@ -65,7 +65,7 @@ jobs:
|
||||
|
||||
- name: Cache NPM
|
||||
id: npm-cache
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: "~/.npm"
|
||||
key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
147
.github/workflows/test.yml
vendored
147
.github/workflows/test.yml
vendored
@ -14,13 +14,11 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
|
||||
testing:
|
||||
name: Run tests
|
||||
typecheck:
|
||||
name: Run typechecking
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@ -56,17 +54,81 @@ jobs:
|
||||
- name: Run typechecking
|
||||
run: npm run test:types
|
||||
|
||||
- name: Run tests
|
||||
testing:
|
||||
name: Run tests - ${{ matrix.test-group.name }}
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
test-group:
|
||||
- name: Browser
|
||||
paths: apps/browser bitwarden_license/bit-browser
|
||||
artifact: jest-coverage-browser
|
||||
junit: junit-browser.xml
|
||||
- name: Web
|
||||
paths: apps/web bitwarden_license/bit-web
|
||||
artifact: jest-coverage-web
|
||||
junit: junit-web.xml
|
||||
- name: Desktop
|
||||
paths: apps/desktop
|
||||
artifact: jest-coverage-desktop
|
||||
junit: junit-desktop.xml
|
||||
- name: CLI
|
||||
paths: apps/cli bitwarden_license/bit-cli
|
||||
artifact: jest-coverage-cli
|
||||
junit: junit-cli.xml
|
||||
- name: Libs
|
||||
paths: libs bitwarden_license/bit-common
|
||||
artifact: jest-coverage-libs
|
||||
junit: junit-libs.xml
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Get Node Version
|
||||
id: retrieve-node-version
|
||||
run: |
|
||||
NODE_NVMRC=$(cat .nvmrc)
|
||||
NODE_VERSION=${NODE_NVMRC/v/''}
|
||||
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests - ${{ matrix.test-group.name }}
|
||||
# maxWorkers is a workaround for a memory leak that crashes tests in CI:
|
||||
# https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
|
||||
run: npm test -- --coverage --maxWorkers=3
|
||||
# Reduced to 2 workers and split tests across parallel jobs to prevent OOM kills
|
||||
run: npm test -- ${{ matrix.test-group.paths }} --coverage --maxWorkers=2
|
||||
env:
|
||||
JEST_JUNIT_OUTPUT_NAME: ${{ matrix.test-group.junit }}
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "junit.xml"
|
||||
name: Test Results - ${{ matrix.test-group.name }}
|
||||
path: ${{ matrix.test-group.junit }}
|
||||
reporter: jest-junit
|
||||
fail-on-error: true
|
||||
|
||||
@ -78,7 +140,7 @@ jobs:
|
||||
- name: Upload test coverage
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: jest-coverage
|
||||
name: ${{ matrix.test-group.artifact }}
|
||||
path: ./coverage/lcov.info
|
||||
|
||||
rust:
|
||||
@ -183,11 +245,35 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download jest coverage
|
||||
- name: Download Browser coverage
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jest-coverage
|
||||
path: ./
|
||||
name: jest-coverage-browser
|
||||
path: ./jest-coverage-browser
|
||||
|
||||
- name: Download Web coverage
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jest-coverage-web
|
||||
path: ./jest-coverage-web
|
||||
|
||||
- name: Download Desktop coverage
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jest-coverage-desktop
|
||||
path: ./jest-coverage-desktop
|
||||
|
||||
- name: Download CLI coverage
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jest-coverage-cli
|
||||
path: ./jest-coverage-cli
|
||||
|
||||
- name: Download Libs coverage
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: jest-coverage-libs
|
||||
path: ./jest-coverage-libs
|
||||
|
||||
- name: Download rust coverage
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
@ -199,5 +285,40 @@ jobs:
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
files: |
|
||||
./lcov.info
|
||||
./jest-coverage-browser/lcov.info
|
||||
./jest-coverage-web/lcov.info
|
||||
./jest-coverage-desktop/lcov.info
|
||||
./jest-coverage-cli/lcov.info
|
||||
./jest-coverage-libs/lcov.info
|
||||
./apps/desktop/desktop_native/lcov.info
|
||||
|
||||
run-tests: # Verifies all required tests complete successfully
|
||||
name: Run tests
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs:
|
||||
- typecheck
|
||||
- testing
|
||||
- rust
|
||||
- rust-coverage
|
||||
- upload-codecov
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check job results
|
||||
env:
|
||||
NEEDS: ${{ toJSON(needs) }}
|
||||
run: |
|
||||
# Print status of all jobs
|
||||
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
|
||||
|
||||
# Collect failed jobs
|
||||
failed_jobs=$(echo "$NEEDS" | jq -r 'to_entries[] | select(.value.result != "success") | .key' | tr '\n' ' ')
|
||||
|
||||
if [ -n "$failed_jobs" ]; then
|
||||
echo "::error::The following jobs failed:$failed_jobs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All required jobs passed successfully!"
|
||||
|
||||
@ -52,7 +52,7 @@ foreach ($subBuildPath in $subBuildPaths) {
|
||||
"--verbose",
|
||||
"--force",
|
||||
"--sign",
|
||||
"588E3F1724AE018EBA762E42279DAE85B313E3ED",
|
||||
"A579B6AE496B360642D05B8AB1B650C1B143B770",
|
||||
"--entitlements",
|
||||
$entitlementsPath
|
||||
)
|
||||
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "لا"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "الموقع"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "البريد الإلكتروني"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "الهاتف"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Keçid açarı ilə giriş et"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Kilidi keçid açarı ilə aç"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Vahid daxil olma üsulunu istifadə et"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Xeyr"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Keçidə sahib olan hər kəs"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Sizin təyin etdiyiniz parola sahib hər kəs"
|
||||
},
|
||||
"location": {
|
||||
"message": "Yerləşmə"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-poçt"
|
||||
},
|
||||
"emails": {
|
||||
"message": "E-poçtlar"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -2477,7 +2486,7 @@
|
||||
"message": "Element birdəfəlik silindi"
|
||||
},
|
||||
"archivedItemRestored": {
|
||||
"message": "Archived item restored"
|
||||
"message": "Arxivlənmiş element bərpa edildi"
|
||||
},
|
||||
"restoreItem": {
|
||||
"message": "Elementi bərpa et"
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Xəta"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Kilid keçid açarı ilə açılmadı. Lütfən yenidən sınayın, ya da başqa kilid açma üsulunu sınayın."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Kilidi açmaq üçün PRF dəstəkli keçid açarı yoxdur. Lütfən əvvəlcə keçid açarı ilə giriş edin."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Şifrə açma xətası"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Qoşmanı endir"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Bitwarden-i endir"
|
||||
},
|
||||
@ -5683,7 +5695,7 @@
|
||||
"message": "Ekstra enli"
|
||||
},
|
||||
"narrow": {
|
||||
"message": "Narrow"
|
||||
"message": "Dar"
|
||||
},
|
||||
"sshKeyWrongPassword": {
|
||||
"message": "Daxil etdiyiniz parol yanlışdır."
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Yan naviqasiyanı yeni. ölçüləndir"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Kimlər baxa bilər"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Xüsusi insanlar"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "Bu Send keçidini paylaşdıqdan sonra, bu \"Send\"ə baxması üçün insanlar e-poçtlarını bir kodla doğrulamalıdırlar."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Birdən çox e-poçtu daxil edərkən vergül istifadə edin."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Не"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Электронная пошта"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Тэлефон"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Вписване със секретен ключ"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Отключване със секретен ключ"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Използване на еднократна идентификация"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Не"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Всеки с връзката"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Всеки с парола, зададена от Вас"
|
||||
},
|
||||
"location": {
|
||||
"message": "Местоположение"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Електронна поща"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Е-пощи"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Телефон"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Грешка"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Отключването със секретен ключ не беше успешно. Опитайте отново или използвайте друг начин за отключване."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Няма секретни ключове с включено PRF, налични за отключване. Първо се впишете със секретен ключ."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Грешка при дешифриране"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Сваляне на прикачения файл"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Сваляне на Битуорден"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Преоразмеряване на страничната навигация"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Кой може да преглежда"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Определени хора"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "След като споделите тази връзка към Изпращане, хората ще трябва да потвърдят е-пощата си чрез код, за да могат да видят това Изпращане."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Можете да въведете повече е-пощи, като ги разделите със запетая."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "потребител@bitwarden.com , потребител@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "না"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "ই-মেইল"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "ফোন"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Ubicació"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Correu electrònic"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telèfon"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Ne"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Kdokoli s odkazem"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Kdokoli s heslem od Vás"
|
||||
},
|
||||
"location": {
|
||||
"message": "Umístění"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "E-maily"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Stáhnout přílohu"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Stáhnout Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Změnit velikost boční navigace"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Kdo může zobrazit"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Vybraní lidé"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "Po sdílení tohoto odkazu Send budou muset jednotlivci ověřit svůj e-mail pomocí kódu pro zobrazení tohoto Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Zadejte více e-mailů oddělených čárkou."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Na"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Lleoliad"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Ebost"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Ffôn"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nej"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Mit Passkey anmelden"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Mit Passkey entsperren"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Single Sign-On verwenden"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nein"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Standort"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-Mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Fehler"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Entsperren mit Passkey fehlgeschlagen. Bitte versuche es erneut oder verwende eine andere Entsperrmethode."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Es sind keine PRF-fähigen Passkeys zum Entsperren verfügbar. Bitte melde dich zuerst mit einem Passkey an."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Entschlüsselungsfehler"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Anhang herunterladen"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Bitwarden herunterladen"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Größe der Seitennavigation ändern"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Όχι"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Τοποθεσία"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Τηλέφωνο"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Λήψη του Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3035,10 +3035,6 @@
|
||||
"custom": {
|
||||
"message": "Custom"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"createSend": {
|
||||
"message": "New Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
@ -6144,5 +6140,9 @@
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
},
|
||||
"sendPasswordHelperText": {
|
||||
"message": "Individuals will need to enter the password to view this Send",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Ubicación"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Correo electrónico"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Teléfono"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Descargar Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Ei"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-post"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefoninumber"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Ez"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Emaila"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefonoa"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "خیر"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "موقعیت"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "ایمیل"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "تلفن"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "بارگیری Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "En"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Sijainti"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Sähköposti"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Puhelinnumero"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Lataa Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Hindi"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Mag-email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telepono"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Non"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Emplacement"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Courriel"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Téléphone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Télécharger Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Non"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Correo electrónico"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Teléfono"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "לא"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "מיקום"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "אימייל"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "טלפון"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "הורד את Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
"message": "bitwarden"
|
||||
},
|
||||
"appLogoLabel": {
|
||||
"message": "Bitwarden logo"
|
||||
"message": "बिटवार्डन लोगो"
|
||||
},
|
||||
"extName": {
|
||||
"message": "बिटवार्डन पासवर्ड मैनेजर",
|
||||
@ -26,16 +26,16 @@
|
||||
"message": "बिटवार्डन का परिचय"
|
||||
},
|
||||
"logInWithPasskey": {
|
||||
"message": "Log in with passkey"
|
||||
"message": "पासकी से लॉग इन करें"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "पासकी से अनलॉक करें"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "सिंगल साइन-ऑन प्रयोग करें"
|
||||
},
|
||||
"yourOrganizationRequiresSingleSignOn": {
|
||||
"message": "Your organization requires single sign-on."
|
||||
"message": "आपके संगठन को सिंगल साइन-ऑन करना आवश्यक है।"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "आपका पुन: स्वागत है!"
|
||||
@ -71,7 +71,7 @@
|
||||
"message": "मास्टर पासवर्ड संकेत आपको भूल जाने की अवस्था में पासवर्ड को याद करने में सहायता करता है।"
|
||||
},
|
||||
"masterPassHintText": {
|
||||
"message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.",
|
||||
"message": "अगर आप अपना पासवर्ड भूल गए हैं, तो पासवर्ड संकेत आपके ईमेल पर भेजा जा सकता है। $CURRENT$/$MAXIMUM$ अक्षर अधिकतम।",
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"content": "$1",
|
||||
@ -90,7 +90,7 @@
|
||||
"message": "Master Password Hint (optional)"
|
||||
},
|
||||
"passwordStrengthScore": {
|
||||
"message": "Password strength score $SCORE$",
|
||||
"message": "पासवर्ड की मज़बूती का स्कोर $SCORE$",
|
||||
"placeholders": {
|
||||
"score": {
|
||||
"content": "$1",
|
||||
@ -99,10 +99,10 @@
|
||||
}
|
||||
},
|
||||
"joinOrganization": {
|
||||
"message": "Join organization"
|
||||
"message": "ऑर्गनाइज़ेशन में शामिल हों"
|
||||
},
|
||||
"joinOrganizationName": {
|
||||
"message": "Join $ORGANIZATIONNAME$",
|
||||
"message": "$ORGANIZATIONNAME$ से जुड़ें",
|
||||
"placeholders": {
|
||||
"organizationName": {
|
||||
"content": "$1",
|
||||
@ -111,7 +111,7 @@
|
||||
}
|
||||
},
|
||||
"finishJoiningThisOrganizationBySettingAMasterPassword": {
|
||||
"message": "Finish joining this organization by setting a master password."
|
||||
"message": "मास्टर पासवर्ड सेट करके इस ऑर्गनाइज़ेशन से जुड़ने की प्रक्रिया पूरी करें।"
|
||||
},
|
||||
"tab": {
|
||||
"message": "टैब"
|
||||
@ -138,7 +138,7 @@
|
||||
"message": "Copy Password"
|
||||
},
|
||||
"copyPassphrase": {
|
||||
"message": "Copy passphrase"
|
||||
"message": "पासफ़्रेज़ कॉपी करें"
|
||||
},
|
||||
"copyNote": {
|
||||
"message": "Copy Note"
|
||||
@ -162,22 +162,22 @@
|
||||
"message": "कंपनी के नाम को कॉपी करें"
|
||||
},
|
||||
"copySSN": {
|
||||
"message": "Copy Social Security number"
|
||||
"message": "सामाजिक सुरक्षा संख्या या आधारकार्ड संख्या कॉपी करें"
|
||||
},
|
||||
"copyPassportNumber": {
|
||||
"message": "Copy passport number"
|
||||
"message": "पासपोर्ट नंबर कॉपी करें"
|
||||
},
|
||||
"copyLicenseNumber": {
|
||||
"message": "Copy license number"
|
||||
},
|
||||
"copyPrivateKey": {
|
||||
"message": "Copy private key"
|
||||
"message": "प्राइवेट की कॉपी करें"
|
||||
},
|
||||
"copyPublicKey": {
|
||||
"message": "Copy public key"
|
||||
},
|
||||
"copyFingerprint": {
|
||||
"message": "Copy fingerprint"
|
||||
"message": "फिंगरप्रिंट कॉपी करें"
|
||||
},
|
||||
"copyCustomField": {
|
||||
"message": "Copy $FIELD$",
|
||||
@ -195,11 +195,11 @@
|
||||
"message": "Copy notes"
|
||||
},
|
||||
"copy": {
|
||||
"message": "Copy",
|
||||
"message": "कॉपी करें",
|
||||
"description": "Copy to clipboard"
|
||||
},
|
||||
"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": {
|
||||
@ -215,7 +215,7 @@
|
||||
"message": "स्वचालित पहचान विवरण"
|
||||
},
|
||||
"fillVerificationCode": {
|
||||
"message": "Fill verification code"
|
||||
"message": "सत्यापन कोड भरें"
|
||||
},
|
||||
"fillVerificationCodeAria": {
|
||||
"message": "Fill Verification Code",
|
||||
@ -261,16 +261,16 @@
|
||||
"message": "Add Item"
|
||||
},
|
||||
"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": "अपना अकाउंट ईमेल पता डालें और आपको आपका पासवर्ड संकेत भेज दिया जाएगा"
|
||||
},
|
||||
"getMasterPasswordHint": {
|
||||
"message": "मास्टर पासवर्ड संकेत प्राप्त करें"
|
||||
@ -297,7 +297,7 @@
|
||||
"message": "Change Master Password"
|
||||
},
|
||||
"continueToWebApp": {
|
||||
"message": "Continue to web app?"
|
||||
"message": "वेब ऐप पर जारी रखें?"
|
||||
},
|
||||
"continueToWebAppDesc": {
|
||||
"message": "Explore more features of your Bitwarden account on the web app."
|
||||
@ -368,7 +368,7 @@
|
||||
"message": "Free Bitwarden Families"
|
||||
},
|
||||
"freeBitwardenFamiliesPageDesc": {
|
||||
"message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app."
|
||||
"message": "आप फ्री बिटवर्डन फैमिलीज़ के लिए एलिजिबल हैं। इस ऑफर को आज ही वेब ऐप में रिडीम करें।"
|
||||
},
|
||||
"version": {
|
||||
"message": "संस्करण"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "नहीं"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "ईमेल"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "फोन"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "अटैचमेंट डाउनलोड करें"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Ne"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Lokacija"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-pošta"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Preuzmi Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nem"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Bárki ezzel a hivatkozással"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Bárki az általam beállított jelszóval"
|
||||
},
|
||||
"location": {
|
||||
"message": "Hely"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Email címek"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefonszám"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Melléklet letöltése"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Bitwarden letöltése"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Oldalnavigáció átméretezés"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Ki láthatja"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Adott személyek"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "A Send hivatkozás megosztása után a személyeknek ellenőrizniük kell email címüket egy kóddal a Send megtekintéséhez."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Írjunk be több email címet vesszővel elválasztva."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,7 +228,7 @@
|
||||
"message": "Salin Nama Kolom Pilihan"
|
||||
},
|
||||
"noMatchingLogins": {
|
||||
"message": "Tidak ada info masuk yang cocok."
|
||||
"message": "Tidak ada log masuk yang cocok"
|
||||
},
|
||||
"noCards": {
|
||||
"message": "Tanpa kartu"
|
||||
@ -440,7 +440,7 @@
|
||||
"message": "Sinkronisasi"
|
||||
},
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
"message": "Selaraskan sekarang"
|
||||
},
|
||||
"lastSync": {
|
||||
"message": "Sinkronisasi Terakhir:"
|
||||
@ -554,27 +554,27 @@
|
||||
"message": "Atur ulang pencarian"
|
||||
},
|
||||
"archiveNoun": {
|
||||
"message": "Archive",
|
||||
"message": "Arsip",
|
||||
"description": "Noun"
|
||||
},
|
||||
"archiveVerb": {
|
||||
"message": "Archive",
|
||||
"message": "Arsip",
|
||||
"description": "Verb"
|
||||
},
|
||||
"unArchive": {
|
||||
"message": "Unarchive"
|
||||
},
|
||||
"itemsInArchive": {
|
||||
"message": "Items in archive"
|
||||
"message": "Butir dalam arsip"
|
||||
},
|
||||
"noItemsInArchive": {
|
||||
"message": "No items in archive"
|
||||
"message": "Tidak ada butir dalam arsip"
|
||||
},
|
||||
"noItemsInArchiveDesc": {
|
||||
"message": "Archived items will appear here and will be excluded from general search results and autofill suggestions."
|
||||
"message": "Butir yang diarsipkan akan muncul di sini dan akan dikecualikan dari hasil pencarian umum dan saran isi otomatis."
|
||||
},
|
||||
"itemWasSentToArchive": {
|
||||
"message": "Item was sent to archive"
|
||||
"message": "Butir dikirim ke arsip"
|
||||
},
|
||||
"itemWasUnarchived": {
|
||||
"message": "Item was unarchived"
|
||||
@ -583,7 +583,7 @@
|
||||
"message": "Item was unarchived"
|
||||
},
|
||||
"archiveItem": {
|
||||
"message": "Archive item"
|
||||
"message": "Arsipkan butir"
|
||||
},
|
||||
"archiveItemDialogContent": {
|
||||
"message": "Once archived, this item will be excluded from search results and autofill suggestions."
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Tidak"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Lokasi"
|
||||
},
|
||||
@ -1480,7 +1486,7 @@
|
||||
"message": "Tidak ada lampiran."
|
||||
},
|
||||
"attachmentSaved": {
|
||||
"message": "Lampiran telah disimpan."
|
||||
"message": "Lampiran disimpan"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
@ -1498,7 +1504,7 @@
|
||||
"message": "Berkas untuk dibagikan"
|
||||
},
|
||||
"selectFile": {
|
||||
"message": "Pilih berkas."
|
||||
"message": "Pilih berkas"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
@ -1646,7 +1652,7 @@
|
||||
"message": "Buka dalam tab baru"
|
||||
},
|
||||
"webAuthnAuthenticate": {
|
||||
"message": "Autentikasi dengan WebAuthn."
|
||||
"message": "Autentikasikan WebAuthn"
|
||||
},
|
||||
"readSecurityKey": {
|
||||
"message": "Baca kunci keamanan"
|
||||
@ -1755,7 +1761,7 @@
|
||||
"message": "URL Server Ikon"
|
||||
},
|
||||
"environmentSaved": {
|
||||
"message": "URL dari semua lingkungan telah disimpan."
|
||||
"message": "URL lingkungan disimpan"
|
||||
},
|
||||
"showAutoFillMenuOnFormFields": {
|
||||
"message": "Tampilkan menu isi otomatis pada kolom formulir",
|
||||
@ -1855,7 +1861,7 @@
|
||||
"message": "Pelajari lebih lanjut tentang isi otomatis"
|
||||
},
|
||||
"defaultAutoFillOnPageLoad": {
|
||||
"message": "Konfigurasi autofill standard untuk item login."
|
||||
"message": "Pengaturan isian otomatis baku bagi butir log masuk"
|
||||
},
|
||||
"defaultAutoFillOnPageLoadDesc": {
|
||||
"message": "Setelah mengaktifkan Auto-Fill waktu website terbuka, kamu dapat mengaktifkan atau meng-nonaktifkan feature ini untuk setiap item. Ini adalah konfigurasi standard untuk item yang tidak dikonfigurasi terpisah."
|
||||
@ -1885,7 +1891,7 @@
|
||||
"message": "Isi otomatis identitas yang terakhir digunakan untuk situs web saat ini"
|
||||
},
|
||||
"commandGeneratePasswordDesc": {
|
||||
"message": "Buat dan salin kata sandi acak baru ke papan klip."
|
||||
"message": "Buat dan salin kata sandi acak baru ke papan klip"
|
||||
},
|
||||
"commandLockVaultDesc": {
|
||||
"message": "Kunci brankas"
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telepon"
|
||||
},
|
||||
@ -2504,10 +2513,10 @@
|
||||
"message": "Item yang Diisi Otomatis dan URI Tersimpan"
|
||||
},
|
||||
"autoFillSuccess": {
|
||||
"message": "Item Terisi Otomatis"
|
||||
"message": "Butir terisi otomatis "
|
||||
},
|
||||
"insecurePageWarning": {
|
||||
"message": "Peringatan: Ini adalah halaman HTTP yang tidak aman, dan setiap informasi yang Anda kirim dapat berpotensi terlihat dan diubah oleh orang lain. Login ini awalnya disimpan di halaman aman (HTTPS) "
|
||||
"message": "Peringatan: Ini adalah halaman HTTP yang tidak aman, dan setiap informasi yang Anda kirim dapat berpotensi terlihat dan diubah oleh orang lain. Log masuk ini awalnya disimpan di halaman aman (HTTPS)."
|
||||
},
|
||||
"insecurePageWarningFillPrompt": {
|
||||
"message": "Anda masih ingin mengisi login ini?"
|
||||
@ -3210,7 +3219,7 @@
|
||||
"message": "Persyaratan kebijakan perusahaan telah diterapkan ke pilihan batas waktu Anda"
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Kebijakan organisasi Anda memengaruhi waktu tunggu brankas Anda. Batas maksimal Waktu Tunggu Brankas yang diizinkan adalah $HOURS$ jam dan $MINUTES$ menit",
|
||||
"message": "Kebijakan organisasi Anda telah menata waktu tunggu brankas maksimum yang diizinkan milik Anda ke $HOURS$ jam $MINUTES$ menit.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
@ -3302,7 +3311,7 @@
|
||||
"message": "Hapus Kata Sandi Utama"
|
||||
},
|
||||
"removedMasterPassword": {
|
||||
"message": "Sandi utama dihapus."
|
||||
"message": "Sandi utama dihapus"
|
||||
},
|
||||
"leaveOrganizationConfirmation": {
|
||||
"message": "Apakah Anda yakin ingin meninggalkan organisasi ini?"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Unduh Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Luogo"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefono"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Scarica Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Ridimensiona la navigazione laterale"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "いいえ"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "場所"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "メールアドレス"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "電話番号"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Bitwarden をダウンロード"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "არა"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "ელ-ფოსტა"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "ტელეფონი"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "ಇಲ್ಲ"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "ಇಮೇಲ್"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "ಫೋನ್"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "아니오"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "이메일"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "전화번호"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Ne"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "El. paštas"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefonas"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Pieteikties ar piekļuves atslēgu"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Atslēgt ar piekļuves atslēgu"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Izmantot vienoto pieteikšanos"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nē"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Atrašanās vieta"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-pasts"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Tālrunis"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Kļūda"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Neizdevās atslēgt ar piekļuves atslēgu. Lūgums mēģināt vēlreiz vai izmantot citu atslēgšanas veidu."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Atslēgšanai nav pieejama neviena PRF iespējota piekļuves atslēga. Lūgums vispirms pieteikties ar piekļuves atslēgu."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Atšifrēšanas kļūda"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Lejupielādēt pielikumu"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Lejupielādē Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Mainīt sānu pārvietošanās joslas izmēru"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "തെറ്റ്"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "ഇമെയിൽ"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "ഫോൺ"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nei"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Sted"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-post"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Last ned Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Inloggen met passkey"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Ontgrendelen met passkey"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Single sign-on gebruiken"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nee"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Locatie"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-mailadres"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefoonnummer"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Fout"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Ontgrendelen met passkey mislukt. Probeer het opnieuw of gebruik een andere ontgrendelingsmethode."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Er zijn geen PRF-ingeschakelde passkeys beschikbaar om te ontgrendelen. Log eerst in met een passkey."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Ontsleutelingsfout"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Bijlage downloaden"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Bitwarden downloaden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Formaat zijnavigatie wijzigen"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nie"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Lokalizacja"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Adres e-mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Numer telefonu"
|
||||
},
|
||||
@ -4845,7 +4854,7 @@
|
||||
"message": "Konsola administratora"
|
||||
},
|
||||
"admin": {
|
||||
"message": "Admin"
|
||||
"message": "Administrator"
|
||||
},
|
||||
"automaticUserConfirmation": {
|
||||
"message": "Automatic user confirmation"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Pobierz załącznik"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Pobierz Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Zmień rozmiar nawigacji bocznej"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Conectar-se com chave de acesso"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Desbloquear com chave de acesso"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Usar autenticação única"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Não"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Localização"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefone"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Erro"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Falha no desbloqueio com a chave de acesso. Tente novamente ou use outro método de desbloqueio."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Nenhuma chave de acesso com PRF está disponível para desbloqueio. Conecte-se com uma chave de acesso primeiro."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Erro de descriptografia"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Baixar anexo"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Baixar o Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Redimensionar navegação lateral"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Não"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Qualquer pessoa com o link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Qualquer pessoa com uma palavra-passe definida por si"
|
||||
},
|
||||
"location": {
|
||||
"message": "Localização"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "E-mails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefone"
|
||||
},
|
||||
@ -4067,7 +4076,7 @@
|
||||
}
|
||||
},
|
||||
"inputMinValue": {
|
||||
"message": "O valor do campo tem de ser, pelo menos, $MIN$ caracteres.",
|
||||
"message": "O valor introduzido deve ser, no mínimo, $MIN$.",
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"content": "$1",
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Transferir anexo"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Descarregar o Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Redimensionar navegação lateral"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Quem pode ver"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Pessoas específicas"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "Após partilhar este Send através do link, os indivíduos terão de verificar o e-mail com um código para poderem ver este Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Introduza vários e-mails, separados por vírgula."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "utilizador@bitwarden.com , utilizador@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nu"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Войти с passkey"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Разблокировать при помощи passkey"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Использовать единый вход"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Нет"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Любой, у кого есть ссылка"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Любой, у кого есть установленный вами пароль"
|
||||
},
|
||||
"location": {
|
||||
"message": "Местоположение"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Телефон"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Ошибка"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Не удалось разблокировать с помощью passkey. Пожалуйста, повторите попытку или используйте другой метод разблокировки."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Для разблокировки недоступны passkeys с поддержкой PRF. Пожалуйста, сначала авторизуйтесь, используя passkey."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Ошибка расшифровки"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Скачать вложение"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Скачать Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Изменить размер боковой навигации"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Кто может просматривать"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Конкретные пользователи"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "После того, как вы поделитесь ссылкой на Send, пользователю нужно будет подтвердить свой email кодом, чтобы просмотреть эту Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Введите несколько email, разделяя их запятой."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "නැත"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "ඊ-තැපැල්"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "දුරකථන"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nie"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Ktokoľvek s odkazom"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Ktokoľvek s heslom od vás"
|
||||
},
|
||||
"location": {
|
||||
"message": "Poloha"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "E-maily"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefón"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Stiahnuť prílohu"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Stiahnuť Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Zmeniť veľkosť bočnej navigácie"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Kto môže zobraziť"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Konkrétne osoby"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "Po zdieľaní tohto odkazu na Send budú musieť jednotlivci overiť svoju e-mailovú adresu pomocou kódu na zobrazenie tohto Sendu."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Zadajte viacero e-mailových adries oddelených čiarkou."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "pouzivate@bitwarden.com, pouzivatel@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Ne"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-pošta"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -4947,7 +4956,7 @@
|
||||
"message": "Organization is deactivated"
|
||||
},
|
||||
"owner": {
|
||||
"message": "Owner"
|
||||
"message": "Lastnik"
|
||||
},
|
||||
"selfOwnershipLabel": {
|
||||
"message": "You",
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Не"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Локација"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Имејл"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Телефон"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Преузети Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Logga in med nyckel"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Lås upp med lösennyckel"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Använd Single Sign-On"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Nej"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Vem som helst med länken"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Alla som har ett lösenord inställt av dig"
|
||||
},
|
||||
"location": {
|
||||
"message": "Plats"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-post"
|
||||
},
|
||||
"emails": {
|
||||
"message": "E-post"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Fel"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Det gick inte att låsa upp med lösennyckel. Försök igen eller använd en annan upplåsningsmetod."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Inga PRF-aktiverade lösennycklar finns tillgängliga för upplåsning. Logga in med en lösennyckel först."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Dekrypteringsfel"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Ladda ned bilaga"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Ladda ner Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Ändra storlek på sidnavigering"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Vem kan se"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specifika personer"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "Efter att ha delat denna Send-länk kommer individer att behöva verifiera sin e-post med en kod för att visa denna Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Ange flera e-postadresser genom att separera dem med kommatecken."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "användare@bitwarden.com , användare@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "இல்லை"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "இருப்பிடம்"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "மின்னஞ்சல்"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "தொலைபேசி"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Bitwarden-ஐப் பதிவிறக்கு"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "No"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Location"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Phone"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Download Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "ไม่"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "ตำแหน่งที่ตั้ง"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "อีเมล"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "โทรศัพท์"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "ดาวน์โหลด Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Geçiş anahtarıyla giriş yap"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Kilidi geçiş anahtarıyla aç"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Çoklu oturum açma kullan"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Hayır"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Bağlantıya sahip olan herkes"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Belirlediğiniz parolaya sahip olan herkes"
|
||||
},
|
||||
"location": {
|
||||
"message": "Konum"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "E-posta"
|
||||
},
|
||||
"emails": {
|
||||
"message": "E-postalar"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Telefon"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Hata"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Kilit geçiş anahtarıyla açılamadı. Lütfen yeniden deneyin veya başka bir kilit açma yöntemi kullanın."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Kilit açma için PRF uyumlu bir geçiş anahtarı bulunamadı. Lütfen önce bir geçiş anahtarıyla giriş yapın."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Şifre çözme sorunu"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Ek dosyayı indir"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Bitwarden’ı indirin"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Kenar menüsünü yeniden boyutlandır"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Kim görebilir"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Belirli kişiler"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "E-posta adreslerini virgülle ayırarak yazın."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "kullanici@bitwarden.com , kullanici@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "Увійти з ключем доступу"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "Розблокувати з ключем доступу"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "Використати єдиний вхід"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Ні"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Будь-хто з посиланням"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Будь-хто зі встановленим вами паролем"
|
||||
},
|
||||
"location": {
|
||||
"message": "Розташування"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Е-пошта"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Е-пошти"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Телефон"
|
||||
},
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "Помилка"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "Не вдалося розблокувати за допомогою ключа доступу. Повторіть спробу або скористайтеся іншим способом розблокування."
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "Немає ключів доступу з підтримкою PRF, доступних для розблокування. Спочатку увійдіть з ключем доступу."
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "Помилка розшифрування"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Завантажити вкладення"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Завантажити Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Змінити розмір бічної панелі"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Хто може переглядати"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Певні люди"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "Після того, як ви поділитеся цим посиланням на відправлення, особам необхідно буде підтвердити свої е-пошти за допомогою коду, щоб переглянути це відправлення."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Введіть декілька адрес е-пошти, розділяючи їх комою."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "Không"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "Vị trí"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "Số điện thoại"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "Tải xuống Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Thay đổi kích thước thanh bên"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@
|
||||
"message": "使用通行密钥登录"
|
||||
},
|
||||
"unlockWithPasskey": {
|
||||
"message": "Unlock with passkey"
|
||||
"message": "使用通行密钥解锁"
|
||||
},
|
||||
"useSingleSignOn": {
|
||||
"message": "使用单点登录"
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "否"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "位置"
|
||||
},
|
||||
@ -1846,7 +1852,7 @@
|
||||
"message": "网页加载时如果检测到登录表单,则执行自动填充。"
|
||||
},
|
||||
"experimentalFeature": {
|
||||
"message": "不完整或不信任的网站可以利用页面加载时的自动填充功能。"
|
||||
"message": "被攻破或不受信任的网站可能会利用页面加载时的自动填充功能。"
|
||||
},
|
||||
"learnMoreAboutAutofillOnPageLoadLinkText": {
|
||||
"message": "进一步了解风险"
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "电子邮箱"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "电话"
|
||||
},
|
||||
@ -2243,7 +2252,7 @@
|
||||
}
|
||||
},
|
||||
"passwordSafe": {
|
||||
"message": "没有在已知的数据泄露中发现此密码,它暂时比较安全。"
|
||||
"message": "在任何已知的数据泄露中均未发现此密码。它暂时比较安全。"
|
||||
},
|
||||
"baseDomain": {
|
||||
"message": "基础域名",
|
||||
@ -3054,11 +3063,11 @@
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendExpiresInHoursSingle": {
|
||||
"message": "在接下来的 1 小时内,任何人都可以通过链接访问此 Send。",
|
||||
"message": "在接下来的 1 小时内,拥有此链接的任何人都可以访问此 Send。",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendExpiresInHours": {
|
||||
"message": "在接下来的 $HOURS$ 小时内,任何人都可以通过链接访问此 Send。",
|
||||
"message": "在接下来的 $HOURS$ 小时内,拥有此链接的任何人都可以访问此 Send。",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@ -3068,11 +3077,11 @@
|
||||
}
|
||||
},
|
||||
"sendExpiresInDaysSingle": {
|
||||
"message": "在接下来的 1 天内,任何人都可以通过链接访问此 Send。",
|
||||
"message": "在接下来的 1 天内,拥有此链接的任何人都可以访问此 Send。",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
|
||||
},
|
||||
"sendExpiresInDays": {
|
||||
"message": "在接下来的 $DAYS$ 天内,任何人都可以通过链接访问此 Send。",
|
||||
"message": "在接下来的 $DAYS$ 天内,拥有此链接的任何人都可以访问此 Send。",
|
||||
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@ -3371,10 +3380,10 @@
|
||||
"message": "错误"
|
||||
},
|
||||
"prfUnlockFailed": {
|
||||
"message": "Failed to unlock with passkey. Please try again or use another unlock method."
|
||||
"message": "使用通行密钥解锁失败。请重试或使用其他解锁方式。"
|
||||
},
|
||||
"noPrfCredentialsAvailable": {
|
||||
"message": "No PRF-enabled passkeys are available for unlock. Please log in with a passkey first."
|
||||
"message": "没有可用于解锁的 PRF 通行密钥。请先使用通行密钥登录。"
|
||||
},
|
||||
"decryptionError": {
|
||||
"message": "解密错误"
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "下载附件"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "下载 Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "调整侧边导航栏大小"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -990,6 +990,12 @@
|
||||
"no": {
|
||||
"message": "否"
|
||||
},
|
||||
"noAuth": {
|
||||
"message": "Anyone with the link"
|
||||
},
|
||||
"anyOneWithPassword": {
|
||||
"message": "Anyone with a password set by you"
|
||||
},
|
||||
"location": {
|
||||
"message": "位置"
|
||||
},
|
||||
@ -2048,6 +2054,9 @@
|
||||
"email": {
|
||||
"message": "電子郵件"
|
||||
},
|
||||
"emails": {
|
||||
"message": "Emails"
|
||||
},
|
||||
"phone": {
|
||||
"message": "電話號碼"
|
||||
},
|
||||
@ -5001,6 +5010,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"downloadAttachmentLabel": {
|
||||
"message": "Download Attachment"
|
||||
},
|
||||
"downloadBitwarden": {
|
||||
"message": "下載 Bitwarden"
|
||||
},
|
||||
@ -6117,5 +6129,20 @@
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "調整側邊欄大小"
|
||||
},
|
||||
"whoCanView": {
|
||||
"message": "Who can view"
|
||||
},
|
||||
"specificPeople": {
|
||||
"message": "Specific people"
|
||||
},
|
||||
"emailVerificationDesc": {
|
||||
"message": "After sharing this Send link, individuals will need to verify their email with a code to view this Send."
|
||||
},
|
||||
"enterMultipleEmailsSeparatedByComma": {
|
||||
"message": "Enter multiple emails by separating with a comma."
|
||||
},
|
||||
"emailPlaceholder": {
|
||||
"message": "user@bitwarden.com , user@acme.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
import { NotificationTypes } from "../../notification/abstractions/notification-bar";
|
||||
|
||||
export type NotificationTypeData = {
|
||||
isVaultLocked?: boolean;
|
||||
@ -17,10 +18,26 @@ export type LoginSecurityTaskInfo = {
|
||||
uri: ModifyLoginCipherFormData["uri"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Distinguished from `NotificationTypes` in that this represents the
|
||||
* pre-resolved notification scenario, vs the notification component
|
||||
* (e.g. "Add" and "Change" will be removed
|
||||
* post-`useUndeterminedCipherScenarioTriggeringLogic` migration)
|
||||
*/
|
||||
export const NotificationScenarios = {
|
||||
...NotificationTypes,
|
||||
/** represents scenarios handling saving new and updated ciphers after form submit */
|
||||
Cipher: "cipher",
|
||||
} as const;
|
||||
|
||||
export type NotificationScenario =
|
||||
(typeof NotificationScenarios)[keyof typeof NotificationScenarios];
|
||||
|
||||
export type WebsiteOriginsWithFields = Map<chrome.tabs.Tab["id"], Set<string>>;
|
||||
|
||||
export type ActiveFormSubmissionRequests = Set<chrome.webRequest.WebRequestDetails["requestId"]>;
|
||||
|
||||
/** This type represents an expectation of nullish values being represented as empty strings */
|
||||
export type ModifyLoginCipherFormData = {
|
||||
uri: string;
|
||||
username: string;
|
||||
|
||||
@ -249,6 +249,20 @@ describe("AutoSubmitLoginBackground", () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("properly cleans up auto-submit workflows when requestInitiator is falsy but active auto-submit hosts exist", async () => {
|
||||
webRequestDetails.initiator = undefined;
|
||||
jest
|
||||
.spyOn(BrowserApi, "getTab")
|
||||
.mockResolvedValue(mock<chrome.tabs.Tab>({ url: validAutoSubmitUrl, id: 1 }));
|
||||
|
||||
triggerWebRequestOnBeforeRequestEvent(webRequestDetails);
|
||||
await flushPromises();
|
||||
|
||||
expect(autoSubmitLoginBackground["validAutoSubmitHosts"].has(validAutoSubmitHost)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the extension is running on a Safari browser", () => {
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { filter, firstValueFrom, of, switchMap } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@ -64,6 +62,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
this.policyService.policiesByType$(PolicyType.AutomaticAppLogIn, userId),
|
||||
),
|
||||
getFirstPolicy,
|
||||
filter((policy): policy is Policy => policy !== undefined),
|
||||
)
|
||||
.subscribe(this.handleAutoSubmitLoginPolicySubscription.bind(this));
|
||||
}
|
||||
@ -165,7 +164,11 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
details: chrome.webRequest.OnBeforeRequestDetails,
|
||||
): undefined => {
|
||||
const requestInitiator = this.getRequestInitiator(details);
|
||||
const isValidInitiator = this.isValidInitiator(requestInitiator);
|
||||
if (!requestInitiator && this.validAutoSubmitHosts.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValidInitiator = requestInitiator ? this.isValidInitiator(requestInitiator) : false;
|
||||
|
||||
if (
|
||||
this.postRequestEncounteredAfterSubmission(details, isValidInitiator) ||
|
||||
@ -175,14 +178,20 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidInitiator && this.shouldRouteTriggerAutoSubmit(details, requestInitiator)) {
|
||||
if (
|
||||
requestInitiator &&
|
||||
isValidInitiator &&
|
||||
this.shouldRouteTriggerAutoSubmit(details, requestInitiator)
|
||||
) {
|
||||
this.setupAutoSubmitFlow(details);
|
||||
return;
|
||||
}
|
||||
|
||||
this.disableAutoSubmitFlow(requestInitiator, details).catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
if (requestInitiator || this.validAutoSubmitHosts.size > 0) {
|
||||
this.disableAutoSubmitFlow(requestInitiator || "", details).catch((error) =>
|
||||
this.logService.error(error),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -368,8 +377,9 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
}
|
||||
|
||||
const tab = await BrowserApi.getTab(details.tabId);
|
||||
if (this.isValidAutoSubmitHost(tab?.url)) {
|
||||
this.removeUrlFromAutoSubmitHosts(tab.url);
|
||||
const tabUrl = tab?.url;
|
||||
if (tabUrl && this.isValidAutoSubmitHost(tabUrl)) {
|
||||
this.removeUrlFromAutoSubmitHosts(tabUrl);
|
||||
}
|
||||
};
|
||||
|
||||
@ -427,7 +437,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
*/
|
||||
private getUrlHost = (url: string) => {
|
||||
let parsedUrl = url;
|
||||
if (!parsedUrl) {
|
||||
if (!parsedUrl || typeof parsedUrl !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
@ -495,6 +505,10 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
message: AutoSubmitLoginMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) => {
|
||||
if (sender.frameId == null || !sender.tab || !message.pageDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.autofillService.doAutoFillOnTab(
|
||||
[
|
||||
{
|
||||
@ -515,7 +529,9 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
* @param sender - The message sender.
|
||||
*/
|
||||
private handleMultiStepAutoSubmitLoginComplete = (sender: chrome.runtime.MessageSender) => {
|
||||
this.removeUrlFromAutoSubmitHosts(sender.url);
|
||||
if (sender.url) {
|
||||
this.removeUrlFromAutoSubmitHosts(sender.url);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -526,7 +542,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
*/
|
||||
private async initSafari() {
|
||||
const currentTab = await BrowserApi.getTabFromCurrentWindow();
|
||||
if (currentTab) {
|
||||
if (currentTab?.url && currentTab.id != null && currentTab.id >= 0) {
|
||||
this.setMostRecentIdpHost(currentTab.url, currentTab.id);
|
||||
}
|
||||
|
||||
@ -558,7 +574,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
}
|
||||
|
||||
const tab = await BrowserApi.getTab(activeInfo.tabId);
|
||||
if (tab) {
|
||||
if (tab?.url && tab.id != null && tab.id >= 0) {
|
||||
this.setMostRecentIdpHost(tab.url, tab.id);
|
||||
}
|
||||
};
|
||||
@ -570,7 +586,7 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
* @param changeInfo - The change information of the tab.
|
||||
*/
|
||||
private handleSafariTabOnUpdated = (tabId: number, changeInfo: chrome.tabs.OnUpdatedInfo) => {
|
||||
if (changeInfo) {
|
||||
if (changeInfo.url) {
|
||||
this.setMostRecentIdpHost(changeInfo.url, tabId);
|
||||
}
|
||||
};
|
||||
@ -626,13 +642,17 @@ export class AutoSubmitLoginBackground implements AutoSubmitLoginBackgroundAbstr
|
||||
* @param sender - The message sender.
|
||||
* @param sendResponse - The response callback.
|
||||
*/
|
||||
private handleExtensionMessage = async (
|
||||
private handleExtensionMessage = (
|
||||
message: AutoSubmitLoginMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response?: any) => void,
|
||||
) => {
|
||||
const { tab, url } = sender;
|
||||
if (tab?.id !== this.currentAutoSubmitHostData.tabId || !this.isValidAutoSubmitHost(url)) {
|
||||
if (
|
||||
!url ||
|
||||
tab?.id !== this.currentAutoSubmitHostData.tabId ||
|
||||
!this.isValidAutoSubmitHost(url)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@ import {
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums/product-tier-type.enum";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
@ -79,6 +80,30 @@ import {
|
||||
} from "./abstractions/overlay-notifications.background";
|
||||
import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.background";
|
||||
|
||||
const inputScenarios = {
|
||||
usernamePasswordNewPassword: "usernamePasswordNewPassword",
|
||||
usernameNewPassword: "usernameNewPassword",
|
||||
usernamePassword: "usernamePassword",
|
||||
username: "username",
|
||||
passwordNewPassword: "passwordNewPassword",
|
||||
newPassword: "newPassword",
|
||||
password: "password",
|
||||
} as const;
|
||||
|
||||
type InputScenarioKey = keyof typeof inputScenarios;
|
||||
type InputScenario = (typeof inputScenarios)[InputScenarioKey];
|
||||
|
||||
type CiphersByInputMatchCategory = {
|
||||
allFieldMatches: CipherView["id"][];
|
||||
newPasswordOnlyMatches: CipherView["id"][];
|
||||
noFieldMatches: CipherView["id"][];
|
||||
passwordNewPasswordMatches: CipherView["id"][];
|
||||
passwordOnlyMatches: CipherView["id"][];
|
||||
usernameNewPasswordMatches: CipherView["id"][];
|
||||
usernameOnlyMatches: CipherView["id"][];
|
||||
usernamePasswordMatches: CipherView["id"][];
|
||||
};
|
||||
|
||||
export default class NotificationBackground {
|
||||
private openUnlockPopout = openUnlockPopout;
|
||||
private openAddEditVaultItemPopout = openAddEditVaultItemPopout;
|
||||
@ -152,6 +177,10 @@ export default class NotificationBackground {
|
||||
this.cleanupNotificationQueue();
|
||||
}
|
||||
|
||||
useUndeterminedCipherScenarioTriggeringLogic$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.UseUndeterminedCipherScenarioTriggeringLogic,
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the enableChangedPasswordPrompt setting from the user notification settings service.
|
||||
*/
|
||||
@ -292,7 +321,7 @@ export default class NotificationBackground {
|
||||
type: CipherType.Login,
|
||||
reprompt,
|
||||
favorite,
|
||||
...(organizationCategories.length ? { organizationCategories } : {}),
|
||||
...(organizationCategories.length > 0 ? { organizationCategories } : {}),
|
||||
icon: buildCipherIcon(iconsServerUrl, view, showFavicons),
|
||||
login: login && { username: login.username },
|
||||
};
|
||||
@ -309,7 +338,7 @@ export default class NotificationBackground {
|
||||
activeUserId: UserId,
|
||||
): Promise<LoginSecurityTaskInfo | null> {
|
||||
const tasks: SecurityTask[] = await this.getSecurityTasks(activeUserId);
|
||||
if (!tasks?.length) {
|
||||
if (!(tasks?.length > 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -317,7 +346,7 @@ export default class NotificationBackground {
|
||||
modifyLoginData.uri,
|
||||
activeUserId,
|
||||
);
|
||||
if (!urlCiphers?.length) {
|
||||
if (!(urlCiphers?.length > 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -596,6 +625,216 @@ export default class NotificationBackground {
|
||||
await this.checkNotificationQueue(tab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives filled form values and determines if a notification should be
|
||||
* triggered, and if so, what kind and with what data.
|
||||
*
|
||||
* If an update scenario is identified, a change password message is added to the
|
||||
* notification queue, prompting the user to update a stored login that has changed.
|
||||
*
|
||||
* A new cipher notification is triggered in other defined scenarios
|
||||
* with the user's form input.
|
||||
*
|
||||
* Returns `true` or `false` to indicate if such a notification was
|
||||
* triggered or not.
|
||||
*
|
||||
* For the purposes of this function, form field inputs should be assumed to be
|
||||
* qualified accurately.
|
||||
*/
|
||||
async triggerCipherNotification(
|
||||
data: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
): Promise<boolean> {
|
||||
const usernameFieldValue: string | null = data.username || null;
|
||||
const currentPasswordFieldValue = data.password || null;
|
||||
const newPasswordFieldValue = data.newPassword || null;
|
||||
|
||||
// If no values were entered, exit early
|
||||
if (!usernameFieldValue && !currentPasswordFieldValue && !newPasswordFieldValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the entered data doesn't have an associated URI, exit early
|
||||
const loginDomain = Utils.getDomain(data.uri);
|
||||
if (loginDomain === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no cipher add/update notifications are enabled, we can exit early
|
||||
const changePasswordNotificationIsEnabled = await this.getEnableChangedPasswordPrompt();
|
||||
const newLoginNotificationIsEnabled = await this.getEnableAddedLoginPrompt();
|
||||
if (!changePasswordNotificationIsEnabled && !newLoginNotificationIsEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is no account logged in (as opposed to only being locked), exit early
|
||||
const authStatus = await this.getAuthStatus();
|
||||
if (authStatus === AuthenticationStatus.LoggedOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is no active user, exit early
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
if (activeUserId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedUsername: string = usernameFieldValue ? usernameFieldValue.toLowerCase() : "";
|
||||
const currentPasswordFieldHasValue =
|
||||
typeof currentPasswordFieldValue === "string" && currentPasswordFieldValue.length > 0;
|
||||
const newPasswordFieldHasValue =
|
||||
typeof newPasswordFieldValue === "string" && newPasswordFieldValue.length > 0;
|
||||
const usernameFieldHasValue =
|
||||
typeof usernameFieldValue === "string" && usernameFieldValue.length > 0;
|
||||
|
||||
// If the current and new password inputs both have values and those values
|
||||
// match, return early, since no change was made
|
||||
if (
|
||||
currentPasswordFieldHasValue &&
|
||||
newPasswordFieldHasValue &&
|
||||
currentPasswordFieldValue === newPasswordFieldValue
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* We only show the unlock notification if a new password field was filled, since
|
||||
* it's very likely to blindly represent an updated cipher value whereas other
|
||||
* scenarios below require the vault to be unlocked in order to determine
|
||||
* if an update has been made.
|
||||
*/
|
||||
if (authStatus === AuthenticationStatus.Locked) {
|
||||
if (!newPasswordFieldHasValue) {
|
||||
return false;
|
||||
}
|
||||
// This needs to be the call that includes the full form data
|
||||
await this.pushChangePasswordToQueue(null, loginDomain, newPasswordFieldValue, tab, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const ciphersForURL: CipherView[] = await this.cipherService.getAllDecryptedForUrl(
|
||||
data.uri,
|
||||
activeUserId,
|
||||
);
|
||||
|
||||
// Reducer structured to avoid subsequent array iterations
|
||||
const ciphersByInputMatchCategory = ciphersForURL.reduce(
|
||||
(acc, { id, login }) => {
|
||||
const usernameInputMatchesCipher =
|
||||
usernameFieldHasValue && login.username?.toLowerCase() === normalizedUsername;
|
||||
const passwordInputMatchesCipher =
|
||||
currentPasswordFieldHasValue && login.password === currentPasswordFieldValue;
|
||||
const newPasswordInputMatchesCipher =
|
||||
newPasswordFieldHasValue && login.password === newPasswordFieldValue;
|
||||
|
||||
if (
|
||||
!newPasswordInputMatchesCipher &&
|
||||
!usernameInputMatchesCipher &&
|
||||
!passwordInputMatchesCipher
|
||||
) {
|
||||
return { ...acc, noFieldMatches: [...acc.noFieldMatches, id] };
|
||||
} else if (
|
||||
newPasswordInputMatchesCipher &&
|
||||
usernameInputMatchesCipher &&
|
||||
passwordInputMatchesCipher
|
||||
) {
|
||||
// Note: this case should be unreachable due to the early exit comparing
|
||||
// the password input values against each other, but leaving this bit here
|
||||
// as a defense against future changes to the pre-match checks.
|
||||
return { ...acc, allFieldMatches: [...acc.allFieldMatches, id] };
|
||||
} else if (
|
||||
newPasswordInputMatchesCipher &&
|
||||
!usernameInputMatchesCipher &&
|
||||
!passwordInputMatchesCipher
|
||||
) {
|
||||
return { ...acc, newPasswordOnlyMatches: [...acc.newPasswordOnlyMatches, id] };
|
||||
} else if (
|
||||
passwordInputMatchesCipher &&
|
||||
!usernameInputMatchesCipher &&
|
||||
!newPasswordInputMatchesCipher
|
||||
) {
|
||||
return { ...acc, passwordOnlyMatches: [...acc.passwordOnlyMatches, id] };
|
||||
} else if (
|
||||
passwordInputMatchesCipher &&
|
||||
newPasswordInputMatchesCipher &&
|
||||
!usernameInputMatchesCipher
|
||||
) {
|
||||
// Note: this case should be unreachable due to the early exit comparing
|
||||
// the password input values against each other, but leaving this bit here
|
||||
// as a defense against future changes to the pre-match checks.
|
||||
return { ...acc, passwordNewPasswordMatches: [...acc.passwordNewPasswordMatches, id] };
|
||||
} else if (
|
||||
usernameInputMatchesCipher &&
|
||||
!passwordInputMatchesCipher &&
|
||||
!newPasswordInputMatchesCipher
|
||||
) {
|
||||
return { ...acc, usernameOnlyMatches: [...acc.usernameOnlyMatches, id] };
|
||||
} else if (
|
||||
usernameInputMatchesCipher &&
|
||||
passwordInputMatchesCipher &&
|
||||
!newPasswordInputMatchesCipher
|
||||
) {
|
||||
return { ...acc, usernamePasswordMatches: [...acc.usernamePasswordMatches, id] };
|
||||
} else if (
|
||||
usernameInputMatchesCipher &&
|
||||
newPasswordInputMatchesCipher &&
|
||||
!passwordInputMatchesCipher
|
||||
) {
|
||||
return { ...acc, usernameNewPasswordMatches: [...acc.usernameNewPasswordMatches, id] };
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
allFieldMatches: [],
|
||||
newPasswordOnlyMatches: [],
|
||||
noFieldMatches: [],
|
||||
passwordNewPasswordMatches: [],
|
||||
passwordOnlyMatches: [],
|
||||
usernameNewPasswordMatches: [],
|
||||
usernameOnlyMatches: [],
|
||||
usernamePasswordMatches: [],
|
||||
},
|
||||
);
|
||||
|
||||
// Handle different field fill combinations and determine the input scenario
|
||||
const inputScenariosByKey = {
|
||||
upn: inputScenarios.usernamePasswordNewPassword,
|
||||
un: inputScenarios.usernameNewPassword,
|
||||
up: inputScenarios.usernamePassword,
|
||||
u: inputScenarios.username,
|
||||
pn: inputScenarios.passwordNewPassword,
|
||||
n: inputScenarios.newPassword,
|
||||
p: inputScenarios.password,
|
||||
} as const;
|
||||
|
||||
type InputScenarioKeys = keyof typeof inputScenariosByKey;
|
||||
|
||||
const key = ((usernameFieldHasValue ? "u" : "") +
|
||||
(currentPasswordFieldHasValue ? "p" : "") +
|
||||
(newPasswordFieldHasValue ? "n" : "")) as InputScenarioKeys;
|
||||
|
||||
const inputScenario = key in inputScenariosByKey ? inputScenariosByKey[key] : null;
|
||||
|
||||
if (inputScenario) {
|
||||
return await this.handleInputMatchScenario({
|
||||
ciphersByInputMatchCategory,
|
||||
ciphersForURL,
|
||||
loginDomain,
|
||||
tab,
|
||||
data,
|
||||
inputScenario,
|
||||
changePasswordNotificationIsEnabled,
|
||||
newLoginNotificationIsEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a change password message to the notification queue, prompting the user
|
||||
* to update the password for a login that has changed.
|
||||
@ -668,13 +907,14 @@ export default class NotificationBackground {
|
||||
|
||||
if (
|
||||
ciphers.length > 0 &&
|
||||
currentPasswordFieldValue?.length &&
|
||||
(currentPasswordFieldValue?.length || 0) > 0 &&
|
||||
// Only use current password for change if no new password present.
|
||||
!newPasswordFieldValue
|
||||
) {
|
||||
const currentPasswordMatchesAnExistingValue = ciphers.some(
|
||||
(cipher) =>
|
||||
cipher.login?.password?.length && cipher.login.password === currentPasswordFieldValue,
|
||||
(cipher.login?.password?.length || 0) > 0 &&
|
||||
cipher.login.password === currentPasswordFieldValue,
|
||||
);
|
||||
|
||||
// The password entered matched a stored cipher value with
|
||||
@ -710,6 +950,213 @@ export default class NotificationBackground {
|
||||
return false;
|
||||
}
|
||||
|
||||
private async handleInputMatchScenario({
|
||||
inputScenario,
|
||||
ciphersByInputMatchCategory,
|
||||
ciphersForURL,
|
||||
loginDomain,
|
||||
tab,
|
||||
data,
|
||||
changePasswordNotificationIsEnabled,
|
||||
newLoginNotificationIsEnabled,
|
||||
}: {
|
||||
ciphersByInputMatchCategory: CiphersByInputMatchCategory;
|
||||
ciphersForURL: CipherView[];
|
||||
loginDomain: string;
|
||||
tab: chrome.tabs.Tab;
|
||||
data: ModifyLoginCipherFormData;
|
||||
inputScenario: InputScenario;
|
||||
changePasswordNotificationIsEnabled: boolean;
|
||||
newLoginNotificationIsEnabled: boolean;
|
||||
}): Promise<boolean> {
|
||||
const {
|
||||
newPasswordOnlyMatches,
|
||||
noFieldMatches,
|
||||
passwordOnlyMatches,
|
||||
usernameNewPasswordMatches,
|
||||
usernameOnlyMatches,
|
||||
usernamePasswordMatches,
|
||||
} = ciphersByInputMatchCategory;
|
||||
// IMPORTANT! The order of statements matters here; later evaluations
|
||||
// depend on the assumptions of the early exits in preceding logic
|
||||
|
||||
// If no ciphers match any filled input values
|
||||
// (Note, this block may uniquely exit early since this match scenario
|
||||
// involves all ciphers, making it mutually exclusive from any other scenario)
|
||||
if (noFieldMatches.length === ciphersForURL.length) {
|
||||
// trigger a new cipher notification in these input scenarios
|
||||
if (
|
||||
(
|
||||
[
|
||||
inputScenarios.usernamePasswordNewPassword,
|
||||
inputScenarios.usernameNewPassword,
|
||||
inputScenarios.usernamePassword,
|
||||
inputScenarios.username,
|
||||
inputScenarios.passwordNewPassword,
|
||||
] as InputScenario[]
|
||||
).includes(inputScenario) &&
|
||||
newLoginNotificationIsEnabled
|
||||
) {
|
||||
await this.pushAddLoginToQueue(
|
||||
loginDomain,
|
||||
{ username: data.username, url: data.uri, password: data.newPassword || data.password },
|
||||
tab,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Trigger an update cipher notification with all URI ciphers
|
||||
// in these input scenarios
|
||||
if (
|
||||
([inputScenarios.password, inputScenarios.newPassword] as InputScenario[]).includes(
|
||||
inputScenario,
|
||||
) &&
|
||||
changePasswordNotificationIsEnabled
|
||||
) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
ciphersForURL.map((c) => c.id),
|
||||
loginDomain,
|
||||
// @TODO handle empty strings / incomplete data structure
|
||||
data.newPassword || data.password,
|
||||
tab,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If ciphers match entered username and new password values
|
||||
if (usernameNewPasswordMatches.length > 0) {
|
||||
// Early exit in these scenarios as they represent "no change"
|
||||
if (
|
||||
(
|
||||
[
|
||||
inputScenarios.usernamePasswordNewPassword,
|
||||
inputScenarios.usernameNewPassword,
|
||||
] as InputScenario[]
|
||||
).includes(inputScenario)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If ciphers match entered username and password values
|
||||
if (usernamePasswordMatches.length > 0) {
|
||||
// and username, password, and new password values were entered
|
||||
if (
|
||||
inputScenario === inputScenarios.usernamePasswordNewPassword &&
|
||||
changePasswordNotificationIsEnabled
|
||||
) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
usernamePasswordMatches,
|
||||
loginDomain,
|
||||
// @TODO handle empty strings / incomplete data structure
|
||||
data.newPassword || data.password,
|
||||
tab,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (inputScenario === inputScenarios.usernamePassword) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If ciphers match entered username value (only)
|
||||
if (usernameOnlyMatches.length > 0) {
|
||||
if (
|
||||
(
|
||||
[
|
||||
inputScenarios.usernamePasswordNewPassword,
|
||||
inputScenarios.usernameNewPassword,
|
||||
inputScenarios.usernamePassword,
|
||||
] as InputScenario[]
|
||||
).includes(inputScenario) &&
|
||||
changePasswordNotificationIsEnabled
|
||||
) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
usernameOnlyMatches,
|
||||
loginDomain,
|
||||
// @TODO handle empty strings / incomplete data structure
|
||||
data.newPassword || data.password,
|
||||
tab,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Early exit in this scenario as it represents "no change"
|
||||
if (inputScenario === inputScenarios.username) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If ciphers match entered new password value (only)
|
||||
if (newPasswordOnlyMatches.length > 0) {
|
||||
// Early exit in these scenarios
|
||||
if (
|
||||
(
|
||||
[
|
||||
inputScenarios.usernameNewPassword, // unclear user expectation
|
||||
inputScenarios.password, // likely nothing to change
|
||||
inputScenarios.newPassword, // nothing to change
|
||||
] as InputScenario[]
|
||||
).includes(inputScenario)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// and username, password, and new password values were entered
|
||||
if (
|
||||
inputScenario === inputScenarios.usernamePasswordNewPassword &&
|
||||
newLoginNotificationIsEnabled
|
||||
) {
|
||||
await this.pushAddLoginToQueue(
|
||||
loginDomain,
|
||||
{ username: data.username, url: data.uri, password: data.newPassword || data.password },
|
||||
tab,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If ciphers match entered password value (only)
|
||||
if (passwordOnlyMatches.length > 0) {
|
||||
if (
|
||||
(
|
||||
[
|
||||
inputScenarios.usernamePasswordNewPassword,
|
||||
inputScenarios.usernamePassword,
|
||||
inputScenarios.passwordNewPassword,
|
||||
] as InputScenario[]
|
||||
).includes(inputScenario) &&
|
||||
changePasswordNotificationIsEnabled
|
||||
) {
|
||||
await this.pushChangePasswordToQueue(
|
||||
passwordOnlyMatches,
|
||||
loginDomain,
|
||||
// @TODO handle empty strings / incomplete data structure
|
||||
data.newPassword || data.password,
|
||||
tab,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Early exit in this scenario as it represents "no change"
|
||||
if (inputScenario === inputScenarios.password) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the page details to the notification bar. Will query all
|
||||
* forms with a password field and pass them to the notification bar.
|
||||
@ -730,6 +1177,7 @@ export default class NotificationBackground {
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO this needs the whole input record, and not just newPassword
|
||||
private async pushChangePasswordToQueue(
|
||||
cipherIds: CipherView["id"][],
|
||||
loginDomain: string,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||
@ -32,6 +33,7 @@ describe("OverlayNotificationsBackground", () => {
|
||||
jest.useFakeTimers();
|
||||
logService = mock<LogService>();
|
||||
notificationBackground = mock<NotificationBackground>();
|
||||
notificationBackground.useUndeterminedCipherScenarioTriggeringLogic$ = of(false);
|
||||
getEnableChangedPasswordPromptSpy = jest
|
||||
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
|
||||
.mockResolvedValue(true);
|
||||
@ -323,6 +325,7 @@ describe("OverlayNotificationsBackground", () => {
|
||||
const pageDetails = mock<AutofillPageDetails>({ fields: [mock<AutofillField>()] });
|
||||
let notificationChangedPasswordSpy: jest.SpyInstance;
|
||||
let notificationAddLoginSpy: jest.SpyInstance;
|
||||
let cipherNotificationSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
sender = mock<chrome.runtime.MessageSender>({
|
||||
@ -334,6 +337,7 @@ describe("OverlayNotificationsBackground", () => {
|
||||
"triggerChangedPasswordNotification",
|
||||
);
|
||||
notificationAddLoginSpy = jest.spyOn(notificationBackground, "triggerAddLoginNotification");
|
||||
cipherNotificationSpy = jest.spyOn(notificationBackground, "triggerCipherNotification");
|
||||
|
||||
sendMockExtensionMessage(
|
||||
{ command: "collectPageDetailsResponse", details: pageDetails },
|
||||
@ -456,6 +460,7 @@ describe("OverlayNotificationsBackground", () => {
|
||||
const pageDetails = mock<AutofillPageDetails>({ fields: [mock<AutofillField>()] });
|
||||
|
||||
beforeEach(async () => {
|
||||
notificationBackground.useUndeterminedCipherScenarioTriggeringLogic$ = of(false);
|
||||
sendMockExtensionMessage(
|
||||
{ command: "collectPageDetailsResponse", details: pageDetails },
|
||||
sender,
|
||||
@ -519,6 +524,44 @@ describe("OverlayNotificationsBackground", () => {
|
||||
expect(notificationAddLoginSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("with `useUndeterminedCipherScenarioTriggeringLogic` on, waits for the tab's navigation to complete using the web navigation API before initializing the notification", async () => {
|
||||
notificationBackground.useUndeterminedCipherScenarioTriggeringLogic$ = of(true);
|
||||
chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => {
|
||||
callback(
|
||||
mock<chrome.tabs.Tab>({
|
||||
status: "loading",
|
||||
url: sender.url,
|
||||
}),
|
||||
);
|
||||
});
|
||||
triggerWebRequestOnCompletedEvent(
|
||||
mock<chrome.webRequest.OnCompletedDetails>({
|
||||
url: sender.url,
|
||||
tabId: sender.tab.id,
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => {
|
||||
callback(
|
||||
mock<chrome.tabs.Tab>({
|
||||
status: "complete",
|
||||
url: sender.url,
|
||||
}),
|
||||
);
|
||||
});
|
||||
triggerWebNavigationOnCompletedEvent(
|
||||
mock<chrome.webNavigation.WebNavigationFramedCallbackDetails>({
|
||||
tabId: sender.tab.id,
|
||||
url: sender.url,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherNotificationSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("initializes the notification immediately when the tab's navigation is complete", async () => {
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
@ -552,6 +595,40 @@ describe("OverlayNotificationsBackground", () => {
|
||||
expect(notificationAddLoginSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("with `useUndeterminedCipherScenarioTriggeringLogic` on, initializes the notification immediately when the tab's navigation is complete", async () => {
|
||||
notificationBackground.useUndeterminedCipherScenarioTriggeringLogic$ = of(true);
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
command: "formFieldSubmitted",
|
||||
uri: "example.com",
|
||||
username: "username",
|
||||
password: "password",
|
||||
newPassword: "newPassword",
|
||||
},
|
||||
sender,
|
||||
);
|
||||
await flushPromises();
|
||||
chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => {
|
||||
callback(
|
||||
mock<chrome.tabs.Tab>({
|
||||
status: "complete",
|
||||
url: sender.url,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
triggerWebRequestOnCompletedEvent(
|
||||
mock<chrome.webRequest.OnCompletedDetails>({
|
||||
url: sender.url,
|
||||
tabId: sender.tab.id,
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherNotificationSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("triggers the notification on the beforeRequest listener when a post-submission redirection is encountered", async () => {
|
||||
sender.tab = mock<chrome.tabs.Tab>({ id: 4 });
|
||||
sendMockExtensionMessage(
|
||||
@ -601,6 +678,57 @@ describe("OverlayNotificationsBackground", () => {
|
||||
|
||||
expect(notificationChangedPasswordSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("with `useUndeterminedCipherScenarioTriggeringLogic` on, triggers the notification on the beforeRequest listener when a post-submission redirection is encountered", async () => {
|
||||
notificationBackground.useUndeterminedCipherScenarioTriggeringLogic$ = of(true);
|
||||
sender.tab = mock<chrome.tabs.Tab>({ id: 4 });
|
||||
sendMockExtensionMessage(
|
||||
{ command: "collectPageDetailsResponse", details: pageDetails },
|
||||
sender,
|
||||
);
|
||||
await flushPromises();
|
||||
sendMockExtensionMessage(
|
||||
{
|
||||
command: "formFieldSubmitted",
|
||||
uri: "example.com",
|
||||
username: "",
|
||||
password: "password",
|
||||
newPassword: "newPassword",
|
||||
},
|
||||
sender,
|
||||
);
|
||||
await flushPromises();
|
||||
chrome.tabs.get = jest.fn().mockImplementation((tabId, callback) => {
|
||||
callback(
|
||||
mock<chrome.tabs.Tab>({
|
||||
status: "complete",
|
||||
url: sender.url,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
triggerWebRequestOnBeforeRequestEvent(
|
||||
mock<chrome.webRequest.WebRequestDetails>({
|
||||
url: sender.url,
|
||||
tabId: sender.tab.id,
|
||||
method: "POST",
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
triggerWebRequestOnBeforeRequestEvent(
|
||||
mock<chrome.webRequest.WebRequestDetails>({
|
||||
url: "https://example.com/redirect",
|
||||
tabId: sender.tab.id,
|
||||
method: "GET",
|
||||
requestId,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(cipherNotificationSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { Subject, switchMap, timer } from "rxjs";
|
||||
import { firstValueFrom, Subject, switchMap, timer } from "rxjs";
|
||||
|
||||
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationType, NotificationTypes } from "../notification/abstractions/notification-bar";
|
||||
import { generateDomainMatchPatterns, isInvalidResponseStatusCode } from "../utils";
|
||||
|
||||
import {
|
||||
@ -14,6 +13,8 @@ import {
|
||||
OverlayNotificationsBackground as OverlayNotificationsBackgroundInterface,
|
||||
OverlayNotificationsExtensionMessage,
|
||||
OverlayNotificationsExtensionMessageHandlers,
|
||||
NotificationScenarios,
|
||||
NotificationScenario,
|
||||
WebsiteOriginsWithFields,
|
||||
} from "./abstractions/overlay-notifications.background";
|
||||
import NotificationBackground from "./notification.background";
|
||||
@ -32,7 +33,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
collectPageDetailsResponse: ({ message, sender }) =>
|
||||
this.handleCollectPageDetailsResponse(message, sender),
|
||||
};
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private notificationBackground: NotificationBackground,
|
||||
@ -281,7 +281,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
|
||||
const shouldAttemptAddNotification = this.shouldAttemptNotification(
|
||||
modifyLoginData,
|
||||
NotificationTypes.Add,
|
||||
NotificationScenarios.Add,
|
||||
);
|
||||
|
||||
if (shouldAttemptAddNotification) {
|
||||
@ -290,7 +290,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
|
||||
const shouldAttemptChangeNotification = this.shouldAttemptNotification(
|
||||
modifyLoginData,
|
||||
NotificationTypes.Change,
|
||||
NotificationScenarios.Change,
|
||||
);
|
||||
|
||||
if (shouldAttemptChangeNotification) {
|
||||
@ -445,29 +445,45 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
requestId: chrome.webRequest.WebRequestDetails["requestId"],
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
tab: chrome.tabs.Tab,
|
||||
config: { skippable: NotificationType[] } = { skippable: [] },
|
||||
config: { skippable: NotificationScenario[] } = { skippable: [] },
|
||||
) => {
|
||||
const notificationCandidates = [
|
||||
{
|
||||
type: NotificationTypes.Change,
|
||||
trigger: this.notificationBackground.triggerChangedPasswordNotification,
|
||||
},
|
||||
{
|
||||
type: NotificationTypes.Add,
|
||||
trigger: this.notificationBackground.triggerAddLoginNotification,
|
||||
},
|
||||
{
|
||||
type: NotificationTypes.AtRiskPassword,
|
||||
trigger: this.notificationBackground.triggerAtRiskPasswordNotification,
|
||||
},
|
||||
].filter(
|
||||
const useUndeterminedCipherScenarioTriggeringLogic = await firstValueFrom(
|
||||
this.notificationBackground.useUndeterminedCipherScenarioTriggeringLogic$,
|
||||
);
|
||||
|
||||
const notificationCandidates = useUndeterminedCipherScenarioTriggeringLogic
|
||||
? [
|
||||
{
|
||||
type: NotificationScenarios.Cipher,
|
||||
trigger: this.notificationBackground.triggerCipherNotification,
|
||||
},
|
||||
{
|
||||
type: NotificationScenarios.AtRiskPassword,
|
||||
trigger: this.notificationBackground.triggerAtRiskPasswordNotification,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: NotificationScenarios.Change,
|
||||
trigger: this.notificationBackground.triggerChangedPasswordNotification,
|
||||
},
|
||||
{
|
||||
type: NotificationScenarios.Add,
|
||||
trigger: this.notificationBackground.triggerAddLoginNotification,
|
||||
},
|
||||
{
|
||||
type: NotificationScenarios.AtRiskPassword,
|
||||
trigger: this.notificationBackground.triggerAtRiskPasswordNotification,
|
||||
},
|
||||
];
|
||||
const filteredNotificationCandidates = notificationCandidates.filter(
|
||||
(candidate) =>
|
||||
this.shouldAttemptNotification(modifyLoginData, candidate.type) ||
|
||||
config.skippable.includes(candidate.type),
|
||||
);
|
||||
|
||||
const results: string[] = [];
|
||||
for (const { trigger, type } of notificationCandidates) {
|
||||
for (const { trigger, type } of filteredNotificationCandidates) {
|
||||
const success = await trigger.bind(this.notificationBackground)(modifyLoginData, tab);
|
||||
if (success) {
|
||||
results.push(`Success: ${type}`);
|
||||
@ -489,8 +505,16 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
*/
|
||||
private shouldAttemptNotification = (
|
||||
modifyLoginData: ModifyLoginCipherFormData,
|
||||
notificationType: NotificationType,
|
||||
notificationType: NotificationScenario,
|
||||
): boolean => {
|
||||
if (notificationType === NotificationScenarios.Cipher) {
|
||||
// The logic after this block pre-qualifies some cipher add/update scenarios
|
||||
// prematurely (where matching against vault contents is required) and should be
|
||||
// skipped for this case (these same checks are performed early in the
|
||||
// notification triggering logic).
|
||||
return true;
|
||||
}
|
||||
|
||||
// Intentionally not stripping whitespace characters here as they
|
||||
// represent user entry.
|
||||
const usernameFieldHasValue = !!(modifyLoginData?.username || "").length;
|
||||
@ -504,15 +528,15 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
|
||||
// `Add` case included because all forms with cached usernames (from previous
|
||||
// visits) will appear to be "password only" and otherwise trigger the new login
|
||||
// save notification.
|
||||
case NotificationTypes.Add:
|
||||
case NotificationScenarios.Add:
|
||||
// Can be values for nonstored login or account creation
|
||||
return usernameFieldHasValue && (passwordFieldHasValue || newPasswordFieldHasValue);
|
||||
case NotificationTypes.Change:
|
||||
case NotificationScenarios.Change:
|
||||
// Can be login with nonstored login changes or account password update
|
||||
return canBeUserLogin || canBePasswordUpdate;
|
||||
case NotificationTypes.AtRiskPassword:
|
||||
case NotificationScenarios.AtRiskPassword:
|
||||
return !newPasswordFieldHasValue;
|
||||
case NotificationTypes.Unlock:
|
||||
case NotificationScenarios.Unlock:
|
||||
// Unlock notifications are handled separately and do not require form data
|
||||
return false;
|
||||
default:
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@ -28,7 +26,7 @@ export default class WebRequestBackground {
|
||||
this.webRequest.onAuthRequired.addListener(
|
||||
(async (
|
||||
details: chrome.webRequest.OnAuthRequiredDetails,
|
||||
callback: (response: chrome.webRequest.BlockingResponse) => void,
|
||||
callback: (response: chrome.webRequest.BlockingResponse | null) => void,
|
||||
) => {
|
||||
if (!details.url || this.pendingAuthRequests.has(details.requestId)) {
|
||||
if (callback) {
|
||||
@ -51,16 +49,16 @@ export default class WebRequestBackground {
|
||||
);
|
||||
|
||||
this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), {
|
||||
urls: ["http://*/*"],
|
||||
urls: ["http://*/*", "https://*/*"],
|
||||
});
|
||||
this.webRequest.onErrorOccurred.addListener((details) => this.completeAuthRequest(details), {
|
||||
urls: ["http://*/*"],
|
||||
urls: ["http://*/*", "https://*/*"],
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveAuthCredentials(
|
||||
domain: string,
|
||||
success: (response: chrome.webRequest.BlockingResponse) => void,
|
||||
success: (response: chrome.webRequest.BlockingResponse | null) => void,
|
||||
// eslint-disable-next-line
|
||||
error: Function,
|
||||
) {
|
||||
@ -82,7 +80,7 @@ export default class WebRequestBackground {
|
||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||
domain,
|
||||
activeUserId,
|
||||
null,
|
||||
undefined,
|
||||
UriMatchStrategy.Host,
|
||||
);
|
||||
if (ciphers == null || ciphers.length !== 1) {
|
||||
@ -90,10 +88,17 @@ export default class WebRequestBackground {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = ciphers[0].login?.username;
|
||||
const password = ciphers[0].login?.password;
|
||||
if (username == null || password == null) {
|
||||
error();
|
||||
return;
|
||||
}
|
||||
|
||||
success({
|
||||
authCredentials: {
|
||||
username: ciphers[0].login.username,
|
||||
password: ciphers[0].login.password,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
|
||||
@ -27,4 +27,5 @@ export function CipherIcon({ color, size, theme, uri }: CipherIconProps) {
|
||||
const cipherIconStyle = ({ width }: { width: string }) => css`
|
||||
width: ${width};
|
||||
height: fit-content;
|
||||
max-height: 24px; /* fallback for Safari */
|
||||
`;
|
||||
|
||||
@ -51,6 +51,7 @@ const cipherItemRowStyles = ({ theme }: { theme: Theme }) => css`
|
||||
background-color: ${themes[theme].background.DEFAULT};
|
||||
padding: ${spacing["2"]} ${spacing["3"]};
|
||||
min-height: min-content;
|
||||
min-height: 36px; /* fallback for Firefox, which doesn't support min-height: min-content on flex items */
|
||||
max-height: 52px;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -8,9 +8,13 @@ import {
|
||||
} from "../../../autofill/content/components/common-types";
|
||||
|
||||
const NotificationTypes = {
|
||||
/** represents scenarios handling saving new ciphers after form submit */
|
||||
Add: "add",
|
||||
/** represents scenarios handling saving updated ciphers after form submit */
|
||||
Change: "change",
|
||||
/** represents scenarios where user has interacted with an unlock action prompt or action otherwise requiring unlock as a prerequisite */
|
||||
Unlock: "unlock",
|
||||
/** represents scenarios where the user has security tasks after updating ciphers */
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
|
||||
@ -139,8 +139,6 @@ import { IpcService, IpcSessionRepository } from "@bitwarden/common/platform/ipc
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||
import {
|
||||
@ -565,36 +563,18 @@ export default class MainBackground {
|
||||
this.memoryStorageService = this.memoryStorageForStateProviders;
|
||||
}
|
||||
|
||||
this.encryptService = new EncryptServiceImplementation(
|
||||
this.cryptoFunctionService,
|
||||
this.logService,
|
||||
true,
|
||||
);
|
||||
|
||||
if (BrowserApi.isManifestVersion(3)) {
|
||||
// Creates a session key for mv3 storage of large memory items
|
||||
const sessionKey = new Lazy(async () => {
|
||||
// Key already in session storage
|
||||
const sessionStorage = new BrowserMemoryStorageService();
|
||||
const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key");
|
||||
if (existingKey) {
|
||||
if (sessionStorage.valuesRequireDeserialization) {
|
||||
return SymmetricCryptoKey.fromJSON(existingKey);
|
||||
}
|
||||
return existingKey;
|
||||
}
|
||||
|
||||
// New key
|
||||
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
|
||||
128,
|
||||
"ephemeral",
|
||||
"bitwarden-ephemeral",
|
||||
);
|
||||
await sessionStorage.save("session-key", derivedKey.toJSON());
|
||||
return derivedKey;
|
||||
});
|
||||
|
||||
this.largeObjectMemoryStorageForStateProviders = new LocalBackedSessionStorageService(
|
||||
sessionKey,
|
||||
new BrowserMemoryStorageService(),
|
||||
this.storageService,
|
||||
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory
|
||||
// and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation`
|
||||
// so that MAC failures are not logged.
|
||||
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
|
||||
this.keyGenerationService,
|
||||
this.encryptService,
|
||||
this.platformUtilsService,
|
||||
this.logService,
|
||||
);
|
||||
@ -629,12 +609,6 @@ export default class MainBackground {
|
||||
storageServiceProvider,
|
||||
);
|
||||
|
||||
this.encryptService = new EncryptServiceImplementation(
|
||||
this.cryptoFunctionService,
|
||||
this.logService,
|
||||
true,
|
||||
);
|
||||
|
||||
this.singleUserStateProvider = new DefaultSingleUserStateProvider(
|
||||
storageServiceProvider,
|
||||
stateEventRegistrarService,
|
||||
|
||||
@ -291,15 +291,21 @@ export default class RuntimeBackground {
|
||||
}
|
||||
break;
|
||||
case "openPopup":
|
||||
await this.openPopup();
|
||||
await this.executeMessageActionOrOpenPopup(msg, this.openPopup.bind(this));
|
||||
break;
|
||||
case VaultMessages.OpenAtRiskPasswords: {
|
||||
await this.main.openAtRisksPasswordsPage();
|
||||
await this.executeMessageActionOrOpenPopup(
|
||||
msg,
|
||||
this.main.openAtRisksPasswordsPage.bind(this),
|
||||
);
|
||||
this.announcePopupOpen();
|
||||
break;
|
||||
}
|
||||
case VaultMessages.OpenBrowserExtensionToUrl: {
|
||||
await this.main.openTheExtensionToPage(msg.url);
|
||||
await this.executeMessageActionOrOpenPopup(
|
||||
msg,
|
||||
this.main.openTheExtensionToPage.bind(this, msg.url),
|
||||
);
|
||||
this.announcePopupOpen();
|
||||
break;
|
||||
}
|
||||
@ -374,40 +380,55 @@ export default class RuntimeBackground {
|
||||
* @param message
|
||||
* @returns true if message fails validation
|
||||
*/
|
||||
private async shouldRejectManyOriginMessage(message: {
|
||||
webExtSender: chrome.runtime.MessageSender;
|
||||
}): Promise<boolean> {
|
||||
private async executeMessageActionOrOpenPopup(
|
||||
message: {
|
||||
webExtSender: chrome.runtime.MessageSender;
|
||||
},
|
||||
messageAction: () => Promise<void>,
|
||||
): Promise<boolean> {
|
||||
const hasAccounts = await firstValueFrom(
|
||||
this.accountService.accounts$.pipe(map((a) => Object.keys(a).length > 0)),
|
||||
);
|
||||
|
||||
// When there are no accounts associated with the extension, only allow opening the popup
|
||||
if (!hasAccounts) {
|
||||
await this.openPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
const isValidVaultReferrer = await this.isValidVaultReferrer(
|
||||
Utils.getHostname(message?.webExtSender?.origin),
|
||||
);
|
||||
|
||||
if (isValidVaultReferrer) {
|
||||
return false;
|
||||
// When the referrer is not a known vault and the message is external, reject the message
|
||||
if (!isValidVaultReferrer && isExternalMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return isExternalMessage(message);
|
||||
await messageAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a message's referrer matches the configured web vault hostname.
|
||||
* Validates that a referrer hostname matches any of the available regions' and current environment web vault URLs.
|
||||
*
|
||||
* @param referrer - hostname from message source
|
||||
* @returns true if referrer matches web vault
|
||||
* @param referrer - hostname from message source (should not include protocol or path)
|
||||
* @returns true if referrer matches any known vault hostname, false otherwise
|
||||
*/
|
||||
private async isValidVaultReferrer(referrer: string | null | undefined): Promise<boolean> {
|
||||
if (!referrer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const vaultUrl = env.getWebVaultUrl();
|
||||
const vaultHostname = Utils.getHostname(vaultUrl);
|
||||
const environment = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
if (!vaultHostname) {
|
||||
return false;
|
||||
}
|
||||
const regions = this.environmentService.availableRegions();
|
||||
const regionVaultUrls = regions.map((r) => r.urls.webVault ?? r.urls.base);
|
||||
const environmentWebVaultUrl = environment.getWebVaultUrl();
|
||||
const messageIsFromKnownVault = [...regionVaultUrls, environmentWebVaultUrl].some(
|
||||
(webVaultUrl) => Utils.getHostname(webVaultUrl) === referrer,
|
||||
);
|
||||
|
||||
return vaultHostname === referrer;
|
||||
return messageIsFromKnownVault;
|
||||
}
|
||||
|
||||
private async autofillPage(tabToAutoFill: chrome.tabs.Tab) {
|
||||
|
||||
@ -560,7 +560,8 @@ export class BrowserApi {
|
||||
* @param event - The event in which to remove the listener from.
|
||||
* @param callback - The callback you want removed from the event.
|
||||
*/
|
||||
static removeListener<T extends (...args: readonly unknown[]) => unknown>(
|
||||
// Chrome's Event.removeListener expects callback args as `any[]` to align with its internal event typings.
|
||||
static removeListener<T extends (...args: readonly any[]) => any>(
|
||||
event: chrome.events.Event<T>,
|
||||
callback: T,
|
||||
) {
|
||||
|
||||
@ -15,6 +15,22 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer
|
||||
return await this.getWithRetries<T>(key, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all storage keys.
|
||||
*
|
||||
* Returns all keys stored in local storage when the browser supports the getKeys API (Chrome 130+).
|
||||
* Returns an empty array on older browser versions where this feature is unavailable.
|
||||
*
|
||||
* @returns Array of storage keys, or empty array if the feature is not supported
|
||||
*/
|
||||
async getKeys(): Promise<string[]> {
|
||||
// getKeys function is only available since Chrome 130
|
||||
if ("getKeys" in this.chromeStorageApi) {
|
||||
return this.chromeStorageApi.getKeys();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async getWithRetries<T>(key: string, retryNum: number): Promise<T> {
|
||||
// See: https://github.com/EFForg/privacybadger/pull/2980
|
||||
const MAX_RETRIES = 5;
|
||||
|
||||
@ -1,20 +1,89 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeStorageService, makeEncString } from "@bitwarden/common/spec";
|
||||
import { FakeStorageService, makeEncString, makeSymmetricCryptoKey } from "@bitwarden/common/spec";
|
||||
import { StorageService } from "@bitwarden/storage-core";
|
||||
|
||||
import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service";
|
||||
import BrowserLocalStorageService from "./browser-local-storage.service";
|
||||
import {
|
||||
LocalBackedSessionStorageService,
|
||||
SessionKeyResolveService,
|
||||
} from "./local-backed-session-storage.service";
|
||||
|
||||
describe("SessionKeyResolveService", () => {
|
||||
let storageService: FakeStorageService;
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let sut: SessionKeyResolveService;
|
||||
|
||||
const mockKey = makeSymmetricCryptoKey();
|
||||
|
||||
beforeEach(() => {
|
||||
storageService = new FakeStorageService();
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
sut = new SessionKeyResolveService(storageService, keyGenerationService);
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("returns null when no session key exists", async () => {
|
||||
const result = await sut.get();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the session key from storage", async () => {
|
||||
await storageService.save("session-key", mockKey);
|
||||
const result = await sut.get();
|
||||
expect(result).toEqual(mockKey);
|
||||
});
|
||||
|
||||
it("deserializes the session key when storage requires deserialization", async () => {
|
||||
const mockStorageService = mock<FakeStorageService>();
|
||||
Object.defineProperty(mockStorageService, "valuesRequireDeserialization", {
|
||||
get: () => true,
|
||||
});
|
||||
mockStorageService.get.mockResolvedValue(mockKey.toJSON());
|
||||
|
||||
const deserializableSut = new SessionKeyResolveService(
|
||||
mockStorageService,
|
||||
keyGenerationService,
|
||||
);
|
||||
|
||||
const result = await deserializableSut.get();
|
||||
|
||||
expect(result).toBeInstanceOf(SymmetricCryptoKey);
|
||||
expect(result?.toJSON()).toEqual(mockKey.toJSON());
|
||||
});
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it("creates a new session key and saves it to storage", async () => {
|
||||
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
||||
salt: "salt",
|
||||
material: new Uint8Array(16) as any,
|
||||
derivedKey: mockKey,
|
||||
});
|
||||
|
||||
const result = await sut.create();
|
||||
|
||||
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledWith(
|
||||
128,
|
||||
"ephemeral",
|
||||
"bitwarden-ephemeral",
|
||||
);
|
||||
expect(result).toEqual(mockKey);
|
||||
expect(await storageService.get("session-key")).toEqual(mockKey.toJSON());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("LocalBackedSessionStorage", () => {
|
||||
const sessionKey = new SymmetricCryptoKey(
|
||||
Utils.fromUtf8ToArray("00000000000000000000000000000000"),
|
||||
);
|
||||
let localStorage: FakeStorageService;
|
||||
const sessionKey = makeSymmetricCryptoKey();
|
||||
let memoryStorage: MockProxy<StorageService>;
|
||||
let keyGenerationService: MockProxy<KeyGenerationService>;
|
||||
let localStorage: MockProxy<BrowserLocalStorageService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
@ -22,14 +91,23 @@ describe("LocalBackedSessionStorage", () => {
|
||||
let sut: LocalBackedSessionStorageService;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage = new FakeStorageService();
|
||||
memoryStorage = mock<StorageService>();
|
||||
keyGenerationService = mock<KeyGenerationService>();
|
||||
localStorage = mock<BrowserLocalStorageService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
logService = mock<LogService>();
|
||||
|
||||
// Default: session key exists
|
||||
memoryStorage.get.mockResolvedValue(sessionKey);
|
||||
Object.defineProperty(memoryStorage, "valuesRequireDeserialization", {
|
||||
get: () => true,
|
||||
});
|
||||
|
||||
sut = new LocalBackedSessionStorageService(
|
||||
new Lazy(async () => sessionKey),
|
||||
memoryStorage,
|
||||
localStorage,
|
||||
keyGenerationService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
logService,
|
||||
@ -37,57 +115,79 @@ describe("LocalBackedSessionStorage", () => {
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("return the cached value when one is cached", async () => {
|
||||
const encString = makeEncString("encrypted");
|
||||
|
||||
it("returns the cached value when one is cached", async () => {
|
||||
sut["cache"]["test"] = "cached";
|
||||
const result = await sut.get("test");
|
||||
expect(result).toEqual("cached");
|
||||
});
|
||||
|
||||
it("returns a decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptString.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.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted"));
|
||||
});
|
||||
it("returns null when both cache and storage are null", async () => {
|
||||
sut["cache"]["test"] = null;
|
||||
localStorage.get.mockResolvedValue(null);
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
await sut.get("test");
|
||||
expect(sut["cache"]["test"]).toEqual("decrypted");
|
||||
const result = await sut.get("test");
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(localStorage.get).toHaveBeenCalledWith("session_test");
|
||||
});
|
||||
|
||||
it("returns a decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
localStorage.get.mockResolvedValue(encString.encryptedString);
|
||||
encryptService.decryptString.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.decryptString).toHaveBeenCalledWith(encrypted, sessionKey),
|
||||
expect(result).toEqual("decrypted"));
|
||||
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(encString, sessionKey);
|
||||
expect(result).toEqual("decrypted");
|
||||
expect(sut["cache"]["test"]).toEqual("decrypted");
|
||||
});
|
||||
|
||||
it("caches the decrypted value when one is stored in local storage", async () => {
|
||||
const encrypted = makeEncString("encrypted");
|
||||
localStorage.internalStore["session_test"] = encrypted.encryptedString;
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
await sut.get("test");
|
||||
expect(sut["cache"]["test"]).toEqual("decrypted");
|
||||
it("returns the cached value when cache is populated during storage retrieval", async () => {
|
||||
localStorage.get.mockImplementation(async () => {
|
||||
sut["cache"]["test"] = "cached-during-read";
|
||||
return encString.encryptedString;
|
||||
});
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted-from-storage"));
|
||||
|
||||
const result = await sut.get("test");
|
||||
|
||||
expect(result).toEqual("cached-during-read");
|
||||
});
|
||||
|
||||
it("returns the cached value when storage returns null but cache was filled", async () => {
|
||||
localStorage.get.mockImplementation(async () => {
|
||||
sut["cache"]["test"] = "cached-during-read";
|
||||
return null;
|
||||
});
|
||||
|
||||
const result = await sut.get("test");
|
||||
|
||||
expect(result).toEqual("cached-during-read");
|
||||
});
|
||||
|
||||
it("creates new session key, clears old data, and returns null when session key is missing", async () => {
|
||||
const newSessionKey = makeSymmetricCryptoKey();
|
||||
const clearSpy = jest.spyOn(sut as any, "clear");
|
||||
memoryStorage.get.mockResolvedValue(null);
|
||||
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
||||
salt: "salt",
|
||||
material: new Uint8Array(16) as any,
|
||||
derivedKey: newSessionKey,
|
||||
});
|
||||
localStorage.get.mockResolvedValue(null);
|
||||
localStorage.getKeys.mockResolvedValue([]);
|
||||
|
||||
const result = await sut.get("test");
|
||||
|
||||
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalled();
|
||||
expect(clearSpy).toHaveBeenCalled();
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("has", () => {
|
||||
it("returns false when the key is not in cache", async () => {
|
||||
const result = await sut.has("test");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when the key is in cache", async () => {
|
||||
sut["cache"]["test"] = "cached";
|
||||
const result = await sut.has("test");
|
||||
@ -95,21 +195,17 @@ describe("LocalBackedSessionStorage", () => {
|
||||
});
|
||||
|
||||
it("returns true when the key is in local storage", async () => {
|
||||
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
|
||||
const encString = makeEncString("encrypted");
|
||||
localStorage.get.mockResolvedValue(encString.encryptedString);
|
||||
encryptService.decryptString.mockResolvedValue(JSON.stringify("decrypted"));
|
||||
const result = await sut.has("test");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it.each([null, undefined])("returns false when %s is cached", async (nullish) => {
|
||||
sut["cache"]["test"] = nullish;
|
||||
await expect(sut.has("test")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it.each([null, undefined])(
|
||||
"returns false when null is stored in local storage",
|
||||
async (nullish) => {
|
||||
localStorage.internalStore["session_test"] = nullish;
|
||||
"returns false when the key does not exist in local storage (%s)",
|
||||
async (value) => {
|
||||
localStorage.get.mockResolvedValue(value);
|
||||
await expect(sut.has("test")).resolves.toBe(false);
|
||||
expect(encryptService.decryptString).not.toHaveBeenCalled();
|
||||
},
|
||||
@ -118,6 +214,7 @@ describe("LocalBackedSessionStorage", () => {
|
||||
|
||||
describe("save", () => {
|
||||
const encString = makeEncString("encrypted");
|
||||
|
||||
beforeEach(() => {
|
||||
encryptService.encryptString.mockResolvedValue(encString);
|
||||
});
|
||||
@ -137,29 +234,44 @@ describe("LocalBackedSessionStorage", () => {
|
||||
});
|
||||
|
||||
it("removes the key when saving a null value", async () => {
|
||||
const spy = jest.spyOn(sut, "remove");
|
||||
const removeSpy = jest.spyOn(sut, "remove");
|
||||
await sut.save("test", null);
|
||||
expect(spy).toHaveBeenCalledWith("test");
|
||||
expect(removeSpy).toHaveBeenCalledWith("test");
|
||||
});
|
||||
|
||||
it("saves the value to cache", async () => {
|
||||
it("uses the session key when encrypting", async () => {
|
||||
await sut.save("test", "value");
|
||||
expect(sut["cache"]["test"]).toEqual("value");
|
||||
});
|
||||
|
||||
it("encrypts and saves the value to local storage", async () => {
|
||||
await sut.save("test", "value");
|
||||
expect(memoryStorage.get).toHaveBeenCalledWith("session-key");
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(
|
||||
JSON.stringify("value"),
|
||||
sessionKey,
|
||||
);
|
||||
expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString);
|
||||
});
|
||||
|
||||
it("emits an update", async () => {
|
||||
const spy = jest.spyOn(sut["updatesSubject"], "next");
|
||||
const updateSpy = jest.spyOn(sut["updatesSubject"], "next");
|
||||
await sut.save("test", "value");
|
||||
expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "save" });
|
||||
expect(updateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" });
|
||||
});
|
||||
|
||||
it("creates a new session key when session key is missing before saving", async () => {
|
||||
const newSessionKey = makeSymmetricCryptoKey();
|
||||
memoryStorage.get.mockResolvedValue(null);
|
||||
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
|
||||
salt: "salt",
|
||||
material: new Uint8Array(16) as any,
|
||||
derivedKey: newSessionKey,
|
||||
});
|
||||
localStorage.getKeys.mockResolvedValue([]);
|
||||
|
||||
await sut.save("test", "value");
|
||||
|
||||
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalled();
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(
|
||||
JSON.stringify("value"),
|
||||
newSessionKey,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -171,15 +283,50 @@ describe("LocalBackedSessionStorage", () => {
|
||||
});
|
||||
|
||||
it("removes the key from local storage", async () => {
|
||||
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
|
||||
await sut.remove("test");
|
||||
expect(localStorage.internalStore["session_test"]).toBeUndefined();
|
||||
expect(localStorage.remove).toHaveBeenCalledWith("session_test");
|
||||
});
|
||||
|
||||
it("emits an update", async () => {
|
||||
const spy = jest.spyOn(sut["updatesSubject"], "next");
|
||||
const updateSpy = jest.spyOn(sut["updatesSubject"], "next");
|
||||
await sut.remove("test");
|
||||
expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
|
||||
expect(updateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionStorageKey", () => {
|
||||
it("prefixes keys with session_ prefix", () => {
|
||||
expect(sut["sessionStorageKey"]("test")).toBe("session_test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("only removes keys with session_ prefix", async () => {
|
||||
const removeSpy = jest.spyOn(sut, "remove");
|
||||
localStorage.getKeys.mockResolvedValue([
|
||||
"session_data1",
|
||||
"session_data2",
|
||||
"regular_key",
|
||||
"another_key",
|
||||
"session_data3",
|
||||
"my_session_key",
|
||||
"mysession",
|
||||
"sessiondata",
|
||||
"user_session",
|
||||
]);
|
||||
|
||||
await sut["clear"]();
|
||||
|
||||
expect(removeSpy).toHaveBeenCalledWith("data1");
|
||||
expect(removeSpy).toHaveBeenCalledWith("data2");
|
||||
expect(removeSpy).toHaveBeenCalledWith("data3");
|
||||
expect(removeSpy).not.toHaveBeenCalledWith("regular_key");
|
||||
expect(removeSpy).not.toHaveBeenCalledWith("another_key");
|
||||
expect(removeSpy).not.toHaveBeenCalledWith("my_session_key");
|
||||
expect(removeSpy).not.toHaveBeenCalledWith("mysession");
|
||||
expect(removeSpy).not.toHaveBeenCalledWith("sessiondata");
|
||||
expect(removeSpy).not.toHaveBeenCalledWith("user_session");
|
||||
expect(removeSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Subject } from "rxjs";
|
||||
|
||||
import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@ -12,33 +13,94 @@ import {
|
||||
StorageUpdate,
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { compareValues } from "@bitwarden/common/platform/misc/compare-values";
|
||||
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
|
||||
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { StorageService } from "@bitwarden/storage-core";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
import { MemoryStoragePortMessage } from "../storage/port-messages";
|
||||
import { portName } from "../storage/port-name";
|
||||
|
||||
import BrowserLocalStorageService from "./browser-local-storage.service";
|
||||
|
||||
const SESSION_KEY_PREFIX = "session_";
|
||||
|
||||
/**
|
||||
* Manages an ephemeral session key for encrypting session storage items persisted in local storage.
|
||||
*
|
||||
* The session key is stored in session storage and automatically cleared when the browser session ends
|
||||
* (e.g., browser restart, extension reload). When the session key is unavailable, any encrypted items
|
||||
* in local storage cannot be decrypted and must be cleared to maintain data consistency.
|
||||
*
|
||||
* This provides session-scoped security for sensitive data while using persistent local storage as the backing store.
|
||||
*
|
||||
* @internal Internal implementation detail. Exported only for testing purposes.
|
||||
* Do not use this class directly outside of tests. Use LocalBackedSessionStorageService instead.
|
||||
*/
|
||||
export class SessionKeyResolveService {
|
||||
constructor(
|
||||
private readonly storageService: StorageService,
|
||||
private readonly keyGenerationService: KeyGenerationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the session key from storage.
|
||||
*
|
||||
* @return session key or null when not in storage
|
||||
*/
|
||||
async get(): Promise<SymmetricCryptoKey | null> {
|
||||
const key = await this.storageService.get<SymmetricCryptoKey>("session-key");
|
||||
if (key) {
|
||||
if (this.storageService.valuesRequireDeserialization) {
|
||||
return SymmetricCryptoKey.fromJSON(key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new session key and adds it to underlying storage.
|
||||
*
|
||||
* @return newly created session key
|
||||
*/
|
||||
async create(): Promise<SymmetricCryptoKey> {
|
||||
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
|
||||
128,
|
||||
"ephemeral",
|
||||
"bitwarden-ephemeral",
|
||||
);
|
||||
await this.storageService.save("session-key", derivedKey.toJSON());
|
||||
return derivedKey;
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalBackedSessionStorageService
|
||||
extends AbstractStorageService
|
||||
implements ObservableStorageService
|
||||
{
|
||||
readonly valuesRequireDeserialization = true;
|
||||
private ports: Set<chrome.runtime.Port> = new Set([]);
|
||||
private cache: Record<string, unknown> = {};
|
||||
private updatesSubject = new Subject<StorageUpdate>();
|
||||
readonly valuesRequireDeserialization = true;
|
||||
updates$ = this.updatesSubject.asObservable();
|
||||
private readonly sessionKeyResolveService: SessionKeyResolveService;
|
||||
|
||||
constructor(
|
||||
private readonly sessionKey: Lazy<Promise<SymmetricCryptoKey>>,
|
||||
private readonly localStorage: AbstractStorageService,
|
||||
private readonly memoryStorage: StorageService,
|
||||
private readonly localStorage: BrowserLocalStorageService,
|
||||
private readonly keyGenerationService: KeyGenerationService,
|
||||
private readonly encryptService: EncryptService,
|
||||
private readonly platformUtilsService: PlatformUtilsService,
|
||||
private readonly logService: LogService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.sessionKeyResolveService = new SessionKeyResolveService(
|
||||
this.memoryStorage,
|
||||
this.keyGenerationService,
|
||||
);
|
||||
|
||||
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
|
||||
if (port.name !== portName(chrome.storage.session)) {
|
||||
return;
|
||||
@ -70,20 +132,20 @@ export class LocalBackedSessionStorageService
|
||||
}
|
||||
|
||||
async get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||
if (this.cache[key] !== undefined) {
|
||||
if (this.cache[key] != null) {
|
||||
return this.cache[key] as T;
|
||||
}
|
||||
|
||||
const value = await this.getLocalSessionValue(await this.sessionKey.get(), key);
|
||||
const value = await this.getLocalSessionValue(await this.getSessionKey(), key);
|
||||
|
||||
if (this.cache[key] === undefined && value !== undefined) {
|
||||
if (this.cache[key] == null && value != null) {
|
||||
// Cache is still empty and we just got a value from local/session storage, cache it.
|
||||
this.cache[key] = value;
|
||||
return value as T;
|
||||
} else if (this.cache[key] === undefined && value === undefined) {
|
||||
} else if (this.cache[key] == null && value == null) {
|
||||
// Cache is still empty and we got nothing from local/session storage, no need to modify cache.
|
||||
return value as T;
|
||||
} else if (this.cache[key] !== undefined && value !== undefined) {
|
||||
} else if (this.cache[key] != null && value != null) {
|
||||
// Conflict, somebody wrote to the cache while we were reading from storage
|
||||
// but we also got a value from storage. We assume the cache is more up to date
|
||||
// and use that value.
|
||||
@ -91,7 +153,7 @@ export class LocalBackedSessionStorageService
|
||||
`Conflict while reading from local session storage, both cache and storage have values. Key: ${key}. Using cached value.`,
|
||||
);
|
||||
return this.cache[key] as T;
|
||||
} else if (this.cache[key] !== undefined && value === undefined) {
|
||||
} else if (this.cache[key] != null && value == null) {
|
||||
// Cache was filled after the local/session storage read completed. We got null
|
||||
// from the storage read, but we have a value from the cache, use that.
|
||||
this.logService.warning(
|
||||
@ -136,6 +198,44 @@ export class LocalBackedSessionStorageService
|
||||
this.updatesSubject.next({ key, updateType: "remove" });
|
||||
}
|
||||
|
||||
protected broadcastMessage(data: Omit<MemoryStoragePortMessage, "originator">) {
|
||||
this.ports.forEach((port) => {
|
||||
this.sendMessageTo(port, data);
|
||||
});
|
||||
}
|
||||
|
||||
private async getSessionKey(): Promise<SymmetricCryptoKey> {
|
||||
const sessionKey = await this.sessionKeyResolveService.get();
|
||||
if (sessionKey != null) {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
// Session key is missing (browser restart/extension reload), so all stored session data
|
||||
// cannot be decrypted. Clear all items before creating a new session key.
|
||||
await this.clear();
|
||||
|
||||
return await this.sessionKeyResolveService.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all stored session data.
|
||||
*
|
||||
* Called when the session key is unavailable (typically after browser restart or extension reload),
|
||||
* making all encrypted session data unrecoverable. Prevents orphaned encrypted data from accumulating.
|
||||
*/
|
||||
private async clear() {
|
||||
const keys = (await this.localStorage.getKeys()).filter((key) =>
|
||||
key.startsWith(SESSION_KEY_PREFIX),
|
||||
);
|
||||
this.logService.debug(
|
||||
`[LocalBackedSessionStorageService] Clearing local session storage. Found ${keys}`,
|
||||
);
|
||||
for (const key of keys) {
|
||||
const keyWithoutPrefix = key.substring(SESSION_KEY_PREFIX.length);
|
||||
await this.remove(keyWithoutPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise<unknown> {
|
||||
const local = await this.localStorage.get<string>(this.sessionStorageKey(key));
|
||||
if (local == null) {
|
||||
@ -159,10 +259,7 @@ export class LocalBackedSessionStorageService
|
||||
}
|
||||
|
||||
const valueJson = JSON.stringify(value);
|
||||
const encValue = await this.encryptService.encryptString(
|
||||
valueJson,
|
||||
await this.sessionKey.get(),
|
||||
);
|
||||
const encValue = await this.encryptService.encryptString(valueJson, await this.getSessionKey());
|
||||
await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString);
|
||||
}
|
||||
|
||||
@ -197,12 +294,6 @@ export class LocalBackedSessionStorageService
|
||||
});
|
||||
}
|
||||
|
||||
protected broadcastMessage(data: Omit<MemoryStoragePortMessage, "originator">) {
|
||||
this.ports.forEach((port) => {
|
||||
this.sendMessageTo(port, data);
|
||||
});
|
||||
}
|
||||
|
||||
private sendMessageTo(
|
||||
port: chrome.runtime.Port,
|
||||
data: Omit<MemoryStoragePortMessage, "originator">,
|
||||
@ -214,7 +305,7 @@ export class LocalBackedSessionStorageService
|
||||
}
|
||||
|
||||
private sessionStorageKey(key: string) {
|
||||
return `session_${key}`;
|
||||
return `${SESSION_KEY_PREFIX}${key}`;
|
||||
}
|
||||
|
||||
private compareValues<T>(value1: T, value2: T): boolean {
|
||||
|
||||
@ -110,7 +110,7 @@
|
||||
<bit-item>
|
||||
<a bit-item-content routerLink="/download-bitwarden">
|
||||
<i slot="start" class="bwi bwi-mobile" aria-hidden="true"></i>
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<p class="tw-pr-2">{{ "downloadBitwardenOnAllDevices" | i18n }}</p>
|
||||
<span
|
||||
*ngIf="showDownloadBitwardenNudge$ | async"
|
||||
|
||||
@ -109,7 +109,7 @@ describe("AttachmentsV2Component", () => {
|
||||
});
|
||||
|
||||
it("passes the submit button to the cipher attachments component", () => {
|
||||
const submitBtn = fixture.debugElement.queryAll(By.directive(ButtonComponent))[1]
|
||||
const submitBtn = fixture.debugElement.queryAll(By.directive(ButtonComponent))[0]
|
||||
.componentInstance;
|
||||
|
||||
expect(cipherAttachment.submitBtn()).toEqual(submitBtn);
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
[showRefresh]="showRefresh"
|
||||
(onRefresh)="refreshCurrentTab()"
|
||||
[description]="(showEmptyAutofillTip$ | async) ? ('autofillSuggestionsTip' | i18n) : undefined"
|
||||
isAutofillList
|
||||
showAutofillButton
|
||||
[disableDescriptionMargin]="showEmptyAutofillTip$ | async"
|
||||
[primaryActionAutofill]="clickItemsToAutofillVaultView$ | async"
|
||||
[groupByType]="groupByType()"
|
||||
></app-vault-list-items-container>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { combineLatest, map, Observable } from "rxjs";
|
||||
import { combineLatest, map, Observable, startWith } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
@ -42,6 +42,12 @@ export class AutofillVaultListItemsComponent {
|
||||
*/
|
||||
protected showRefresh: boolean = BrowserPopupUtils.inSidebar(window);
|
||||
|
||||
/** Flag indicating whether the login item should automatically autofill when clicked */
|
||||
protected clickItemsToAutofillVaultView$: Observable<boolean> =
|
||||
this.vaultSettingsService.clickItemsToAutofillVaultView$.pipe(
|
||||
startWith(true), // Start with true to avoid flashing the fill button on first load
|
||||
);
|
||||
|
||||
protected readonly groupByType = toSignal(
|
||||
this.vaultPopupItemsService.hasFilterApplied$.pipe(map((hasFilter) => !hasFilter)),
|
||||
);
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
></button>
|
||||
<bit-menu #moreOptions>
|
||||
@if (!decryptionFailure) {
|
||||
<ng-container *ngIf="canAutofill && showAutofill()">
|
||||
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
|
||||
<ng-container *ngIf="autofillAllowed$ | async">
|
||||
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||
{{ "autofill" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container>
|
||||
<ng-container *ngIf="showViewOption">
|
||||
<button type="button" bitMenuItem (click)="onView()">
|
||||
{{ "view" | i18n }}
|
||||
</button>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { booleanAttribute, Component, input, Input } from "@angular/core";
|
||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { filter } from "rxjs/operators";
|
||||
@ -76,10 +76,22 @@ export class ItemMoreOptionsComponent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag to show the autofill menu options. Used for items that are
|
||||
* Flag to show view item menu option. Used when something else is
|
||||
* assigned as the primary action for the item, such as autofill.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ transform: booleanAttribute })
|
||||
showViewOption = false;
|
||||
|
||||
/**
|
||||
* Flag to hide the autofill menu options. Used for items that are
|
||||
* already in the autofill list suggestion.
|
||||
*/
|
||||
readonly showAutofill = input(false, { transform: booleanAttribute });
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input({ transform: booleanAttribute })
|
||||
hideAutofillOptions = false;
|
||||
|
||||
protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$;
|
||||
|
||||
|
||||
@ -90,11 +90,11 @@
|
||||
</ng-container>
|
||||
|
||||
<cdk-virtual-scroll-viewport [itemSize]="itemHeight$ | async" bitScrollLayout>
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers" class="tw-group/vault-item">
|
||||
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
|
||||
<button
|
||||
bit-item-content
|
||||
type="button"
|
||||
(click)="onCipherSelect(cipher)"
|
||||
(click)="primaryActionOnSelect(cipher)"
|
||||
(dblclick)="launchCipher(cipher)"
|
||||
[appA11yTitle]="
|
||||
cipherItemTitleKey()(cipher)
|
||||
@ -125,14 +125,19 @@
|
||||
</button>
|
||||
|
||||
<ng-container slot="end">
|
||||
<bit-item-action *ngIf="isAutofillList()">
|
||||
<span
|
||||
class="tw-opacity-0 tw-text-sm tw-text-primary-600 tw-px-2 group-hover/vault-item:tw-opacity-100 group-focus-within/vault-item:tw-opacity-100"
|
||||
<bit-item-action *ngIf="!hideAutofillButton()">
|
||||
<button
|
||||
type="button"
|
||||
bitBadge
|
||||
variant="primary"
|
||||
(click)="doAutofill(cipher)"
|
||||
[title]="autofillShortcutTooltip() ?? ('autofillTitle' | i18n: cipher.name)"
|
||||
[attr.aria-label]="'autofillTitle' | i18n: cipher.name"
|
||||
>
|
||||
{{ "fill" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-item-action>
|
||||
<bit-item-action *ngIf="!isAutofillList() && CipherViewLikeUtils.canLaunch(cipher)">
|
||||
<bit-item-action *ngIf="!showAutofillButton() && CipherViewLikeUtils.canLaunch(cipher)">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-external-link"
|
||||
@ -144,7 +149,8 @@
|
||||
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
|
||||
<app-item-more-options
|
||||
[cipher]="cipher"
|
||||
[showAutofill]="!isAutofillList()"
|
||||
[hideAutofillOptions]="hideAutofillMenuOptions()"
|
||||
[showViewOption]="primaryActionAutofill()"
|
||||
></app-item-more-options>
|
||||
</ng-container>
|
||||
</bit-item>
|
||||
|
||||
@ -136,18 +136,24 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
*/
|
||||
private viewCipherTimeout?: number;
|
||||
|
||||
readonly ciphers = input<PopupCipherViewLike[]>([]);
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
ciphers = input<PopupCipherViewLike[]>([]);
|
||||
|
||||
/**
|
||||
* If true, we will group ciphers by type (Login, Card, Identity)
|
||||
* within subheadings in a single container, converted to a WritableSignal.
|
||||
*/
|
||||
readonly groupByType = input<boolean | undefined>(false);
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
groupByType = input<boolean | undefined>(false);
|
||||
|
||||
/**
|
||||
* Computed signal for a grouped list of ciphers with an optional header
|
||||
*/
|
||||
readonly cipherGroups = computed<
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherGroups = computed<
|
||||
{
|
||||
subHeaderKey?: string;
|
||||
ciphers: PopupCipherViewLike[];
|
||||
@ -189,7 +195,9 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Title for the vault list item section.
|
||||
*/
|
||||
readonly title = input<string | undefined>(undefined);
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
title = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Optionally allow the items to be collapsed.
|
||||
@ -197,20 +205,24 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
* The key must be added to the state definition in `vault-popup-section.service.ts` since the
|
||||
* collapsed state is stored locally.
|
||||
*/
|
||||
readonly collapsibleKey = input<keyof PopupSectionOpen | undefined>(undefined);
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
collapsibleKey = input<keyof PopupSectionOpen | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Optional description for the vault list item section. Will be shown below the title even when
|
||||
* no ciphers are available.
|
||||
*/
|
||||
|
||||
readonly description = input<string | undefined>(undefined);
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
description = input<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Option to show a refresh button in the section header.
|
||||
*/
|
||||
|
||||
readonly showRefresh = input(false, { transform: booleanAttribute });
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showRefresh = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Event emitted when the refresh button is clicked.
|
||||
@ -223,16 +235,23 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Flag indicating that the current tab location is blocked
|
||||
*/
|
||||
readonly currentUriIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$);
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
currentURIIsBlocked = toSignal(this.vaultPopupAutofillService.currentTabIsOnBlocklist$);
|
||||
|
||||
/**
|
||||
* Resolved i18n key to use for suggested cipher items
|
||||
*/
|
||||
readonly cipherItemTitleKey = computed(() => {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
cipherItemTitleKey = computed(() => {
|
||||
return (cipher: CipherViewLike) => {
|
||||
const login = CipherViewLikeUtils.getLogin(cipher);
|
||||
const hasUsername = login?.username != null;
|
||||
const key = !this.currentUriIsBlocked() ? "autofillTitle" : "viewItemTitle";
|
||||
const key =
|
||||
this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? "autofillTitle"
|
||||
: "viewItemTitle";
|
||||
return hasUsername ? `${key}WithField` : key;
|
||||
};
|
||||
});
|
||||
@ -240,25 +259,47 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
/**
|
||||
* Option to show the autofill button for each item.
|
||||
*/
|
||||
readonly isAutofillList = input(false, { transform: booleanAttribute });
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
showAutofillButton = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Computed property whether the cipher select action should perform autofill
|
||||
* Flag indicating whether the suggested cipher item autofill button should be shown or not
|
||||
*/
|
||||
readonly shouldAutofillOnSelect = computed(
|
||||
() => this.isAutofillList() && !this.currentUriIsBlocked(),
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillButton = computed(
|
||||
() => !this.showAutofillButton() || this.currentURIIsBlocked() || this.primaryActionAutofill(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Flag indicating whether the cipher item autofill menu options should be shown or not
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
hideAutofillMenuOptions = computed(() => this.currentURIIsBlocked() || this.showAutofillButton());
|
||||
|
||||
/**
|
||||
* Option to perform autofill operation as the primary action for autofill suggestions.
|
||||
*/
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
primaryActionAutofill = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Remove the bottom margin from the bit-section in this component
|
||||
* (used for containers at the end of the page where bottom margin is not needed)
|
||||
*/
|
||||
readonly disableSectionMargin = input(false, { transform: booleanAttribute });
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableSectionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* Remove the description margin
|
||||
*/
|
||||
readonly disableDescriptionMargin = input(false, { transform: booleanAttribute });
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
disableDescriptionMargin = input(false, { transform: booleanAttribute });
|
||||
|
||||
/**
|
||||
* The tooltip text for the organization icon for ciphers that belong to an organization.
|
||||
@ -272,7 +313,9 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
return collections[0]?.name;
|
||||
}
|
||||
|
||||
protected readonly autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
protected autofillShortcutTooltip = signal<string | undefined>(undefined);
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
@ -297,8 +340,10 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
onCipherSelect(cipher: PopupCipherViewLike) {
|
||||
return this.shouldAutofillOnSelect() ? this.doAutofill(cipher) : this.onViewCipher(cipher);
|
||||
primaryActionOnSelect(cipher: PopupCipherViewLike) {
|
||||
return this.primaryActionAutofill() && !this.currentURIIsBlocked()
|
||||
? this.doAutofill(cipher)
|
||||
: this.onViewCipher(cipher);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -9,7 +9,6 @@ import { BehaviorSubject, Observable, Subject, of } from "rxjs";
|
||||
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { NudgeType, NudgesService } from "@bitwarden/angular/vault";
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
import {
|
||||
AutoConfirmExtensionSetupDialogComponent,
|
||||
AutomaticUserConfirmationService,
|
||||
@ -185,7 +184,7 @@ describe("VaultV2Component", () => {
|
||||
filterVisibilityState$: new BehaviorSubject<any>({}),
|
||||
} as Partial<VaultPopupListFiltersService>;
|
||||
|
||||
const accountActive$ = new BehaviorSubject<FakeAccount | null>({ id: "user-1" });
|
||||
const activeAccount$ = new BehaviorSubject<FakeAccount | null>({ id: "user-1" });
|
||||
|
||||
const cipherSvc = {
|
||||
failedToDecryptCiphers$: jest.fn().mockReturnValue(of([])),
|
||||
@ -222,12 +221,6 @@ describe("VaultV2Component", () => {
|
||||
hasPremiumFromAnySource$: (_: string) => hasPremiumFromAnySource$,
|
||||
};
|
||||
|
||||
const vaultProfileSvc = {
|
||||
getProfileCreationDate: jest
|
||||
.fn()
|
||||
.mockResolvedValue(new Date(Date.now() - 8 * 24 * 60 * 60 * 1000)), // 8 days ago
|
||||
};
|
||||
|
||||
const configSvc = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation((_flag: string) => of(false)),
|
||||
};
|
||||
@ -250,16 +243,12 @@ describe("VaultV2Component", () => {
|
||||
{ provide: VaultPopupScrollPositionService, useValue: scrollSvc },
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: { activeAccount$: accountActive$ },
|
||||
useValue: { activeAccount$ },
|
||||
},
|
||||
{ provide: CipherService, useValue: cipherSvc },
|
||||
{ provide: DialogService, useValue: dialogSvc },
|
||||
{ provide: IntroCarouselService, useValue: introSvc },
|
||||
{ provide: NudgesService, useValue: nudgesSvc },
|
||||
{
|
||||
provide: VaultProfileService,
|
||||
useValue: vaultProfileSvc,
|
||||
},
|
||||
{
|
||||
provide: VaultPopupCopyButtonsService,
|
||||
useValue: { showQuickCopyActions$: new BehaviorSubject<boolean>(false) },
|
||||
@ -473,7 +462,7 @@ describe("VaultV2Component", () => {
|
||||
it("dismissVaultNudgeSpotlight forwards to NudgesService with active user id", fakeAsync(() => {
|
||||
const spy = jest.spyOn(nudgesSvc, "dismissNudge").mockResolvedValue(undefined);
|
||||
|
||||
accountActive$.next({ id: "user-xyz" });
|
||||
activeAccount$.next({ id: "user-xyz" });
|
||||
|
||||
void component.ngOnInit();
|
||||
tick();
|
||||
@ -485,6 +474,10 @@ describe("VaultV2Component", () => {
|
||||
}));
|
||||
|
||||
it("accountAgeInDays$ computes integer days since creation", (done) => {
|
||||
activeAccount$.next({
|
||||
id: "user-123",
|
||||
creationDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago
|
||||
} as any);
|
||||
getObs<number | null>(component, "accountAgeInDays$").subscribe((days) => {
|
||||
if (days !== null) {
|
||||
expect(days).toBeGreaterThanOrEqual(7);
|
||||
@ -570,10 +563,6 @@ describe("VaultV2Component", () => {
|
||||
itemsSvc.cipherCount$.next(10);
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
|
||||
vaultProfileSvc.getProfileCreationDate = jest
|
||||
.fn()
|
||||
.mockResolvedValue(new Date(Date.now() - 3 * 24 * 60 * 60 * 1000)); // 3 days ago
|
||||
|
||||
(nudgesSvc.showNudgeSpotlight$ as jest.Mock).mockImplementation((type: NudgeType) => {
|
||||
return of(type === NudgeType.PremiumUpgrade);
|
||||
});
|
||||
|
||||
@ -5,26 +5,24 @@ import { Component, DestroyRef, effect, inject, OnDestroy, OnInit } from "@angul
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router, RouterModule } from "@angular/router";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
withLatestFrom,
|
||||
tap,
|
||||
BehaviorSubject,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
|
||||
import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||
import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component";
|
||||
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
|
||||
import { DeactivatedOrg, NoResults, VaultOpen } from "@bitwarden/assets/svg";
|
||||
import {
|
||||
AutoConfirmExtensionSetupDialogComponent,
|
||||
@ -162,10 +160,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
FeatureFlag.BrowserPremiumSpotlight,
|
||||
);
|
||||
|
||||
private showPremiumNudgeSpotlight$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.nudgesService.showNudgeSpotlight$(NudgeType.PremiumUpgrade, userId)),
|
||||
);
|
||||
|
||||
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
||||
protected allFilters$ = this.vaultPopupListFiltersService.allFilters$;
|
||||
@ -173,38 +167,39 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
protected hasPremium$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.billingAccountService.hasPremiumFromAnySource$(userId)),
|
||||
);
|
||||
protected accountAgeInDays$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => {
|
||||
const creationDate$ = from(this.vaultProfileService.getProfileCreationDate(userId));
|
||||
return creationDate$.pipe(
|
||||
map((creationDate) => {
|
||||
if (!creationDate) {
|
||||
return 0;
|
||||
}
|
||||
const ageInMilliseconds = Date.now() - creationDate.getTime();
|
||||
return Math.floor(ageInMilliseconds / (1000 * 60 * 60 * 24));
|
||||
}),
|
||||
);
|
||||
protected accountAgeInDays$ = this.accountService.activeAccount$.pipe(
|
||||
map((account) => {
|
||||
if (!account || !account.creationDate) {
|
||||
return 0;
|
||||
}
|
||||
const creationDate = account.creationDate;
|
||||
const ageInMilliseconds = Date.now() - creationDate.getTime();
|
||||
return Math.floor(ageInMilliseconds / (1000 * 60 * 60 * 24));
|
||||
}),
|
||||
);
|
||||
|
||||
protected showPremiumSpotlight$ = combineLatest([
|
||||
this.premiumSpotlightFeatureFlag$,
|
||||
this.showPremiumNudgeSpotlight$,
|
||||
this.activeUserId$.pipe(
|
||||
switchMap((userId) =>
|
||||
this.nudgesService.showNudgeSpotlight$(NudgeType.PremiumUpgrade, userId),
|
||||
),
|
||||
),
|
||||
this.showHasItemsVaultSpotlight$,
|
||||
this.hasPremium$,
|
||||
this.cipherCount$,
|
||||
this.accountAgeInDays$,
|
||||
]).pipe(
|
||||
map(
|
||||
([featureFlagEnabled, showPremiumNudge, showHasItemsNudge, hasPremium, count, age]) =>
|
||||
map(([featureFlagEnabled, showPremiumNudge, showHasItemsNudge, hasPremium, count, age]) => {
|
||||
return (
|
||||
featureFlagEnabled &&
|
||||
showPremiumNudge &&
|
||||
!showHasItemsNudge &&
|
||||
!hasPremium &&
|
||||
count >= 5 &&
|
||||
age >= 7,
|
||||
),
|
||||
age >= 7
|
||||
);
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
@ -263,7 +258,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
private router: Router,
|
||||
private autoConfirmService: AutomaticUserConfirmationService,
|
||||
private toastService: ToastService,
|
||||
private vaultProfileService: VaultProfileService,
|
||||
private billingAccountService: BillingAccountProfileStateService,
|
||||
private liveAnnouncer: LiveAnnouncer,
|
||||
private i18nService: I18nService,
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</button>
|
||||
}
|
||||
<ng-container slot="end">
|
||||
@if ((archiveFlagEnabled$ | async) && cipher.isArchived) {
|
||||
@if ((archiveFlagEnabled$ | async) && cipher.isArchived && !cipher.isDeleted) {
|
||||
<button
|
||||
type="button"
|
||||
[bitAction]="unarchive"
|
||||
|
||||
@ -469,7 +469,7 @@ describe("ViewV2Component", () => {
|
||||
|
||||
describe("unarchive button", () => {
|
||||
it("shows the unarchive button when the cipher is archived", fakeAsync(() => {
|
||||
component.cipher = { ...mockCipher, isArchived: true } as CipherView;
|
||||
component.cipher = { ...mockCipher, isArchived: true, isDeleted: false } as CipherView;
|
||||
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
@ -491,6 +491,18 @@ describe("ViewV2Component", () => {
|
||||
);
|
||||
expect(unarchiveBtn).toBeFalsy();
|
||||
}));
|
||||
|
||||
it("does not show the unarchive button when the cipher is deleted", fakeAsync(() => {
|
||||
component.cipher = { ...mockCipher, isArchived: true, isDeleted: true } as CipherView;
|
||||
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
const unarchiveBtn = fixture.debugElement.query(
|
||||
By.css("button[biticonbutton='bwi-unarchive']"),
|
||||
);
|
||||
expect(unarchiveBtn).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
describe("archive", () => {
|
||||
|
||||
@ -50,10 +50,16 @@
|
||||
<vault-permit-cipher-details-popover></vault-permit-cipher-details-popover>
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control disableMargin>
|
||||
<bit-form-control>
|
||||
<input bitCheckbox formControlName="showQuickCopyActions" type="checkbox" />
|
||||
<bit-label>{{ "showQuickCopyActions" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control disableMargin>
|
||||
<input bitCheckbox formControlName="clickItemsToAutofillVaultView" type="checkbox" />
|
||||
<bit-label>
|
||||
{{ "clickToAutofill" | i18n }}
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
</bit-card>
|
||||
</form>
|
||||
</popup-page>
|
||||
|
||||
@ -59,12 +59,14 @@ describe("AppearanceV2Component", () => {
|
||||
const enableRoutingAnimation$ = new BehaviorSubject<boolean>(true);
|
||||
const enableCompactMode$ = new BehaviorSubject<boolean>(false);
|
||||
const showQuickCopyActions$ = new BehaviorSubject<boolean>(false);
|
||||
const clickItemsToAutofillVaultView$ = new BehaviorSubject<boolean>(false);
|
||||
const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
|
||||
const setShowFavicons = jest.fn().mockResolvedValue(undefined);
|
||||
const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
|
||||
const setEnableRoutingAnimation = jest.fn().mockResolvedValue(undefined);
|
||||
const setEnableCompactMode = jest.fn().mockResolvedValue(undefined);
|
||||
const setShowQuickCopyActions = jest.fn().mockResolvedValue(undefined);
|
||||
const setClickItemsToAutofillVaultView = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const mockWidthService: Partial<PopupSizeService> = {
|
||||
width$: new BehaviorSubject("default"),
|
||||
@ -111,7 +113,10 @@ describe("AppearanceV2Component", () => {
|
||||
},
|
||||
{
|
||||
provide: VaultSettingsService,
|
||||
useValue: mock<VaultSettingsService>(),
|
||||
useValue: {
|
||||
clickItemsToAutofillVaultView$,
|
||||
setClickItemsToAutofillVaultView,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -142,6 +147,7 @@ describe("AppearanceV2Component", () => {
|
||||
enableCompactMode: false,
|
||||
showQuickCopyActions: false,
|
||||
width: "default",
|
||||
clickItemsToAutofillVaultView: false,
|
||||
});
|
||||
});
|
||||
|
||||
@ -187,5 +193,11 @@ describe("AppearanceV2Component", () => {
|
||||
|
||||
expect(mockWidthService.setWidth).toHaveBeenCalledWith("wide");
|
||||
});
|
||||
|
||||
it("updates the click items to autofill vault view setting", () => {
|
||||
component.appearanceForm.controls.clickItemsToAutofillVaultView.setValue(true);
|
||||
|
||||
expect(setClickItemsToAutofillVaultView).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user