1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-14 10:26:19 +01:00

Merge branch 'main' into auth/pm-4816/create-shared-LoginApprovalComponent

This commit is contained in:
Alec Rippberger 2024-11-07 15:33:26 -06:00
commit 3cb7ce4597
No known key found for this signature in database
GPG Key ID: 9DD8DA583B28154A
290 changed files with 9698 additions and 6610 deletions

20
.github/renovate.json vendored
View File

@ -73,7 +73,7 @@
"reviewers": ["team:team-admin-console-dev"] "reviewers": ["team:team-admin-console-dev"]
}, },
{ {
"matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious", "regedit"], "matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious"],
"description": "Auth owned dependencies", "description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:", "commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"] "reviewers": ["team:team-auth-dev"]
@ -110,6 +110,8 @@
}, },
{ {
"matchPackageNames": [ "matchPackageNames": [
"@electron/notarize",
"@electron/rebuild",
"@types/argon2-browser", "@types/argon2-browser",
"@types/chrome", "@types/chrome",
"@types/firefox-webext-browser", "@types/firefox-webext-browser",
@ -119,6 +121,12 @@
"argon2", "argon2",
"argon2-browser", "argon2-browser",
"big-integer", "big-integer",
"electron-builder",
"electron-log",
"electron-reload",
"electron-store",
"electron-updater",
"electron",
"node-forge", "node-forge",
"rxjs", "rxjs",
"type-fest", "type-fest",
@ -197,19 +205,11 @@
}, },
{ {
"matchPackageNames": [ "matchPackageNames": [
"@electron/notarize",
"@electron/rebuild",
"@microsoft/signalr-protocol-msgpack", "@microsoft/signalr-protocol-msgpack",
"@microsoft/signalr", "@microsoft/signalr",
"@types/jsdom", "@types/jsdom",
"@types/papaparse", "@types/papaparse",
"@types/zxcvbn", "@types/zxcvbn",
"electron-builder",
"electron-log",
"electron-reload",
"electron-store",
"electron-updater",
"electron",
"jsdom", "jsdom",
"jszip", "jszip",
"oidc-client-ts", "oidc-client-ts",
@ -258,5 +258,5 @@
"reviewers": ["team:team-vault-dev"] "reviewers": ["team:team-vault-dev"]
} }
], ],
"ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm", "regedit"] "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"]
} }

View File

@ -1,7 +1,8 @@
name: Build Browser name: Build Browser
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -33,6 +34,10 @@ defaults:
shell: bash shell: bash
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -41,8 +46,10 @@ jobs:
adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version - name: Get Package Version
id: gen_vars id: gen_vars
@ -71,8 +78,10 @@ jobs:
run: run:
working-directory: apps/browser working-directory: apps/browser
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Testing locales - extName length - name: Testing locales - extName length
run: | run: |
@ -109,8 +118,10 @@ jobs:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -163,10 +174,6 @@ jobs:
run: npm run dist:mv3 run: npm run dist:mv3
working-directory: browser-source/apps/browser working-directory: browser-source/apps/browser
- name: Build Chrome Manifest v3 Beta
run: npm run dist:chrome:beta
working-directory: browser-source/apps/browser
- name: Upload Opera artifact - name: Upload Opera artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
@ -188,13 +195,6 @@ jobs:
path: browser-source/apps/browser/dist/dist-chrome-mv3.zip path: browser-source/apps/browser/dist/dist-chrome-mv3.zip
if-no-files-found: error if-no-files-found: error
- name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip
if-no-files-found: error
- name: Upload Firefox artifact - name: Upload Firefox artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
@ -236,12 +236,15 @@ jobs:
needs: needs:
- setup - setup
- locales-test - locales-test
- check-run
env: env:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -353,8 +356,10 @@ jobs:
- build - build
- build-safari - build-safari
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure - name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -392,7 +397,10 @@ jobs:
- crowdin-push - crowdin-push
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build CLI name: Build CLI
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -34,6 +35,10 @@ defaults:
working-directory: apps/cli working-directory: apps/cli
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -41,8 +46,10 @@ jobs:
package_version: ${{ steps.retrieve-package-version.outputs.package_version }} package_version: ${{ steps.retrieve-package-version.outputs.package_version }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version - name: Get Package Version
id: retrieve-package-version id: retrieve-package-version
@ -58,7 +65,6 @@ jobs:
NODE_VERSION=${NODE_NVMRC/v/''} NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
cli: cli:
name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}" name: "${{ matrix.os.base }} - ${{ matrix.license_type.readable }}"
strategy: strategy:
@ -82,8 +88,10 @@ jobs:
_WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_VERSION: 3.5 _WIN_PKG_VERSION: 3.5
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Unix Vars - name: Setup Unix Vars
run: | run: |
@ -160,8 +168,10 @@ jobs:
_WIN_PKG_FETCH_VERSION: 20.11.1 _WIN_PKG_FETCH_VERSION: 20.11.1
_WIN_PKG_VERSION: 3.5 _WIN_PKG_VERSION: 3.5
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Windows builder - name: Setup Windows builder
run: | run: |
@ -310,8 +320,10 @@ jobs:
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Print environment - name: Print environment
run: | run: |
@ -386,10 +398,14 @@ jobs:
- cli - cli
- cli-windows - cli-windows
- snap - snap
- check-run
steps: steps:
- name: Check if any job failed - name: Check if any job failed
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build Desktop name: Build Desktop
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -32,12 +33,18 @@ defaults:
shell: bash shell: bash
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
electron-verify: electron-verify:
name: Verify Electron Version name: Verify Electron Version
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Verify - name: Verify
run: | run: |
@ -65,8 +72,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Package Version - name: Get Package Version
id: retrieve-version id: retrieve-version
@ -138,8 +147,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -238,7 +249,9 @@ jobs:
windows: windows:
name: Windows Build name: Windows Build
runs-on: windows-2022 runs-on: windows-2022
needs: setup needs:
- setup
- check-run
defaults: defaults:
run: run:
shell: pwsh shell: pwsh
@ -248,8 +261,10 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
NODE_OPTIONS: --max_old_space_size=4096 NODE_OPTIONS: --max_old_space_size=4096
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -447,7 +462,9 @@ jobs:
macos-build: macos-build:
name: MacOS Build name: MacOS Build
runs-on: macos-13 runs-on: macos-13
needs: setup needs:
- setup
- check-run
env: env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }}
@ -456,8 +473,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -622,8 +641,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -841,8 +862,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -1088,8 +1111,10 @@ jobs:
run: run:
working-directory: apps/desktop working-directory: apps/desktop
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -1279,8 +1304,10 @@ jobs:
- macos-package-mas - macos-package-mas
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure - name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -1323,7 +1350,10 @@ jobs:
- crowdin-push - crowdin-push
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,7 +1,8 @@
name: Build Web name: Build Web
on: on:
pull_request: pull_request_target:
types: [opened, synchronize]
branches-ignore: branches-ignore:
- 'l10n_master' - 'l10n_master'
- 'cf-pages' - 'cf-pages'
@ -36,6 +37,10 @@ env:
_AZ_REGISTRY: bitwardenprod.azurecr.io _AZ_REGISTRY: bitwardenprod.azurecr.io
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -43,8 +48,10 @@ jobs:
version: ${{ steps.version.outputs.value }} version: ${{ steps.version.outputs.value }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get GitHub sha as version - name: Get GitHub sha as version
id: version id: version
@ -89,8 +96,10 @@ jobs:
git_metadata: true git_metadata: true
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -142,6 +151,7 @@ jobs:
needs: needs:
- setup - setup
- build-artifacts - build-artifacts
- check-run
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -155,8 +165,10 @@ jobs:
env: env:
_VERSION: ${{ needs.setup.outputs.version }} _VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Check Branch to Publish - name: Check Branch to Publish
env: env:
@ -250,11 +262,15 @@ jobs:
crowdin-push: crowdin-push:
name: Crowdin Push name: Crowdin Push
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
needs: build-artifacts needs:
- build-artifacts
- check-run
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure - name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -282,9 +298,11 @@ jobs:
trigger-web-vault-deploy: trigger-web-vault-deploy:
name: Trigger web vault deploy name: Trigger web vault deploy
if: github.ref == 'refs/heads/main' if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: build-artifacts needs:
- build-artifacts
- check-run
steps: steps:
- name: Login to Azure - CI Subscription - name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -326,7 +344,10 @@ jobs:
- trigger-web-vault-deploy - trigger-web-vault-deploy
steps: steps:
- name: Check if any job failed - name: Check if any job failed
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc') && contains(needs.*.result, 'failure') if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1 run: exit 1
- name: Login to Azure - Prod Subscription - name: Login to Azure - Prod Subscription

View File

@ -1,124 +1,130 @@
name: Version Bump name: Repository management
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
task:
default: "Version Bump"
description: "Task to execute"
options:
- "Version Bump"
- "Version Bump and Cut rc"
required: true
type: choice
bump_browser: bump_browser:
description: "Bump Browser?" description: "Bump Browser version?"
type: boolean type: boolean
default: false default: false
bump_cli: bump_cli:
description: "Bump CLI?" description: "Bump CLI version?"
type: boolean type: boolean
default: false default: false
bump_desktop: bump_desktop:
description: "Bump Desktop?" description: "Bump Desktop version?"
type: boolean type: boolean
default: false default: false
bump_web: bump_web:
description: "Bump Web?" description: "Bump Web version?"
type: boolean type: boolean
default: false default: false
target_ref:
default: "main"
description: "Branch/Tag to target for cut"
required: true
type: string
version_number_override: version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')" description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false required: false
type: string type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
enable_slack_notification:
description: "Enable Slack notifications for upcoming release?"
default: false
type: boolean
jobs: jobs:
setup:
name: Setup
runs-on: ubuntu-24.04
outputs:
branch: ${{ steps.set-branch.outputs.branch }}
token: ${{ steps.app-token.outputs.token }}
steps:
- name: Set branch
id: set-branch
env:
TASK: ${{ inputs.task }}
run: |
if [[ "$TASK" == "Version Bump" ]]; then
BRANCH="none"
elif [[ "$TASK" == "Version Bump and Cut rc" ]]; then
BRANCH="rc"
fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
cut_branch:
name: Cut branch
if: ${{ needs.setup.outputs.branch == 'rc' }}
needs: setup
runs-on: ubuntu-24.04
steps:
- name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.target_ref }}
token: ${{ needs.setup.outputs.token }}
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Cut branch
env:
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: |
git switch --quiet --create $BRANCH_NAME
git push --quiet --set-upstream origin $BRANCH_NAME
bump_version: bump_version:
name: Bump Version name: Bump Version
runs-on: ubuntu-22.04 if: ${{ always() }}
runs-on: ubuntu-24.04
needs:
- cut_branch
- setup
outputs: outputs:
version_browser: ${{ steps.set-final-version-output.outputs.version_browser }} version_browser: ${{ steps.set-final-version-output.outputs.version_browser }}
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }} version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }} version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }}
version_web: ${{ steps.set-final-version-output.outputs.version_web }} version_web: ${{ steps.set-final-version-output.outputs.version_web }}
steps: steps:
- name: Validate version input - name: Validate version input format
if: ${{ inputs.version_number_override != '' }} if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main uses: bitwarden/gh-actions/version-check@main
with: with:
version: ${{ inputs.version_number_override }} version: ${{ inputs.version_number_override }}
- name: Slack Notification Check - name: Check out branch
run: |
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
echo "Slack notifications enabled."
else
echo "Slack notifications disabled."
fi
- name: Checkout Branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: main ref: main
token: ${{ needs.setup.outputs.token }}
- name: Check if RC branch exists - name: Configure Git
if: ${{ inputs.cut_rc_branch == true }}
run: | run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l) git config --local user.email "actions@github.com"
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then git config --local user.name "Github Actions"
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Setup git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create Version Branch
id: create-branch
run: |
CLIENTS=()
if [[ ${{ inputs.bump_browser }} == true ]]; then
CLIENTS+=("browser")
fi
if [[ ${{ inputs.bump_cli }} == true ]]; then
CLIENTS+=("cli")
fi
if [[ ${{ inputs.bump_desktop }} == true ]]; then
CLIENTS+=("desktop")
fi
if [[ ${{ inputs.bump_web }} == true ]]; then
CLIENTS+=("web")
fi
printf -v joined '%s,' "${CLIENTS[@]}"
echo "client=${joined%,}" >> $GITHUB_OUTPUT
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
######################## ########################
# VERSION BUMP SECTION # # VERSION BUMP SECTION #
@ -165,7 +171,9 @@ jobs:
- name: Bump Browser Version - Version Override - name: Bump Browser Version - Version Override
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
id: bump-browser-version-override id: bump-browser-version-override
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/browser $VERSION
- name: Bump Browser Version - Automatic Calculation - name: Bump Browser Version - Automatic Calculation
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
@ -250,7 +258,9 @@ jobs:
- name: Bump CLI Version - Version Override - name: Bump CLI Version - Version Override
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
id: bump-cli-version-override id: bump-cli-version-override
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/cli $VERSION
- name: Bump CLI Version - Automatic Calculation - name: Bump CLI Version - Automatic Calculation
if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
@ -300,7 +310,9 @@ jobs:
- name: Bump Desktop Version - Root - Version Override - name: Bump Desktop Version - Root - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
id: bump-desktop-version-override id: bump-desktop-version-override
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - Root - Automatic Calculation - name: Bump Desktop Version - Root - Automatic Calculation
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
@ -311,7 +323,9 @@ jobs:
- name: Bump Desktop Version - App - Version Override - name: Bump Desktop Version - App - Version Override
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
run: npm version ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version $VERSION
working-directory: "apps/desktop/src" working-directory: "apps/desktop/src"
- name: Bump Desktop Version - App - Automatic Calculation - name: Bump Desktop Version - App - Automatic Calculation
@ -362,7 +376,9 @@ jobs:
- name: Bump Web Version - Version Override - name: Bump Web Version - Version Override
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }} if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
id: bump-web-version-override id: bump-web-version-override
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number_override }} env:
VERSION: ${{ inputs.version_number_override }}
run: npm version --workspace=@bitwarden/web-vault $VERSION
- name: Bump Web Version - Automatic Calculation - name: Bump Web Version - Automatic Calculation
if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }} if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
@ -375,27 +391,29 @@ jobs:
- name: Set final version output - name: Set final version output
id: set-final-version-output id: set-final-version-output
env:
VERSION: ${{ inputs.version_number_override }}
run: | run: |
if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then
echo "version_browser=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_browser=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then
echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then
echo "version_cli=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_cli=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then
echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then
echo "version_desktop=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_desktop=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then
echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then
echo "version_web=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version_web=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then
echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
@ -416,199 +434,52 @@ jobs:
- name: Push changes - name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }} if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env: run: git push
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
- name: Generate PR message
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: pr-message
run: |
MESSAGE=""
if [[ "${{ inputs.bump_browser }}" == "true" ]]; then
MESSAGE+=$' Browser version bump to ${{ steps.set-final-version-output.outputs.version_browser }}\n'
fi
if [[ "${{ inputs.bump_cli }}" == "true" ]]; then cherry_pick:
MESSAGE+=$' CLI version bump to ${{ steps.set-final-version-output.outputs.version_cli }}\n' name: Cherry-Pick Commit(s)
fi if: ${{ needs.setup.outputs.branch == 'rc' }}
runs-on: ubuntu-24.04
if [[ "${{ inputs.bump_desktop }}" == "true" ]]; then needs:
MESSAGE+=$' Desktop version bump to ${{ steps.set-final-version-output.outputs.version_desktop }}\n' - bump_version
fi - setup
if [[ "${{ inputs.bump_web }}" == "true" ]]; then
MESSAGE+=$' Web version bump to ${{ steps.set-final-version-output.outputs.version_web }}\n'
fi
echo "MESSAGE<<EOF" >> $GITHUB_ENV
echo "$MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
owner: ${{ github.repository_owner }}
- name: Create Version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump client(s) version"
run: |
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
$MESSAGE")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
- name: Report upcoming browser release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_browser != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_browser }}
project: browser
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming cli release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_cli != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_cli }}
project: cli
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming desktop release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_desktop != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_desktop }}
project: desktop
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Report upcoming web release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && steps.set-final-version-output.outputs.version_web != '' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version_web }}
project: web
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Branch - name: Check out main branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: main ref: main
token: ${{ needs.setup.outputs.token }}
### Browser - name: Configure Git
- name: Browser - Verify version has been updated
if: ${{ inputs.bump_browser == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_browser }}
run: | run: |
# Wait for version to change. git config --local user.email "actions@github.com"
while : ; do git config --local user.name "Github Actions"
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop. - name: Perform cherry-pick(s)
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/browser
### CLI
- name: CLI - Verify version has been updated
if: ${{ inputs.bump_cli == true }}
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_cli }}
run: | run: |
# Wait for version to change. # Function for cherry-picking
while : ; do cherry_pick () {
echo "Waiting for version to be updated..." local package_path="apps/$1/package.json"
git pull --force local source_branch=$2
CURRENT_VERSION=$(cat package.json | jq -r '.version') local destination_branch=$3
# If the versions don't match we continue the loop, otherwise we break out of the loop. # Get project commit/version from source branch
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break git switch $source_branch
sleep 10 SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 $package_path)
done SOURCE_VERSION=$(cat $package_path | jq -r '.version')
working-directory: apps/cli
### Desktop # Get project commit/version from destination branch
- name: Desktop - Verify version has been updated git switch $destination_branch
if: ${{ inputs.bump_desktop == true }} DESTINATION_VERSION=$(cat $package_path | jq -r '.version')
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version_desktop }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop. if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
sleep 10 git push -u origin $destination_branch
done fi
working-directory: apps/desktop
### Web # Cherry-pick from 'main' into 'rc'
- name: Web - Verify version has been updated cherry_pick browser main rc
if: ${{ inputs.bump_web == true }} cherry_pick cli main rc
env: cherry_pick desktop main rc
NEW_VERSION: ${{ needs.bump_version.outputs.version_web }} cherry_pick web main rc
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
working-directory: apps/web
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc

View File

@ -8,27 +8,55 @@ on:
jobs: jobs:
bump-version: bump-version:
name: Bump Desktop Version name: Bump Desktop Version
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- name: Login to Azure - CI Subscription - name: Generate GH App token
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with: with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
- name: Retrieve bot secrets - name: Check out target ref
id: retrieve-bot-secrets uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with: with:
keyvault: bitwarden-ci ref: main
secrets: "github-pat-bitwarden-devops-bot-repo-scope" token: ${{ steps.app-token.outputs.token }}
- name: Trigger Version Bump workflow - name: Configure Git
env:
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
run: | run: |
echo '{"cut_rc_branch": "false", git config --local user.email "actions@github.com"
"bump_browser": "false", git config --local user.name "Github Actions"
"bump_cli": "false",
"bump_desktop": "true", - name: Get current Desktop version
"bump_web": "false"}' | \ id: current-desktop-version
gh workflow run version-bump.yml --json --repo bitwarden/clients run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
working-directory: apps/desktop
- name: Calculate next Desktop release version
id: calculate-next-desktop-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-desktop-version.outputs.version }}
- name: Bump Desktop Version - Root - Automatic Calculation
id: bump-desktop-version-automatic
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version --workspace=@bitwarden/desktop $VERSION
- name: Bump Desktop Version - App - Automatic Calculation
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: npm version $VERSION
working-directory: "apps/desktop/src"
- name: Commit files
env:
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
run: git commit -m "Bumped Desktop client to $VERSION" -a
- name: Push changes
run: git push

View File

@ -128,10 +128,10 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "test-storybook:build:production" "buildTarget": "test-storybook:build:production"
}, },
"development": { "development": {
"browserTarget": "test-storybook:build:development" "buildTarget": "test-storybook:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

View File

@ -9,7 +9,6 @@ const replace = require("gulp-replace");
const manifest = require("./src/manifest.json"); const manifest = require("./src/manifest.json");
const manifestVersion = parseInt(process.env.MANIFEST_VERSION || manifest.version); const manifestVersion = parseInt(process.env.MANIFEST_VERSION || manifest.version);
const betaBuild = process.env.BETA_BUILD === "1";
const paths = { const paths = {
build: "./build/", build: "./build/",
@ -17,27 +16,11 @@ const paths = {
safari: "./src/safari/", safari: "./src/safari/",
}; };
/**
* Converts a number to a tuple containing two Uint16's
* @param num {number} This number is expected to be a integer style number with no decimals
*
* @returns {number[]} A tuple containing two elements that are both numbers.
*/
function numToUint16s(num) {
var arr = new ArrayBuffer(4);
var view = new DataView(arr);
view.setUint32(0, num, false);
return [view.getUint16(0), view.getUint16(2)];
}
function buildString() { function buildString() {
var build = ""; var build = "";
if (process.env.MANIFEST_VERSION) { if (process.env.MANIFEST_VERSION) {
build = `-mv${process.env.MANIFEST_VERSION}`; build = `-mv${process.env.MANIFEST_VERSION}`;
} }
if (betaBuild) {
build += "-beta";
}
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") { if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
build = `-${process.env.BUILD_NUMBER}`; build = `-${process.env.BUILD_NUMBER}`;
} }
@ -71,9 +54,6 @@ function distFirefox() {
manifest.optional_permissions = manifest.optional_permissions.filter( manifest.optional_permissions = manifest.optional_permissions.filter(
(permission) => permission !== "privacy", (permission) => permission !== "privacy",
); );
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest; return manifest;
}); });
} }
@ -90,9 +70,6 @@ function distOpera() {
delete manifest.commands._execute_sidebar_action; delete manifest.commands._execute_sidebar_action;
} }
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest; return manifest;
}); });
} }
@ -102,9 +79,6 @@ function distChrome() {
delete manifest.applications; delete manifest.applications;
delete manifest.sidebar_action; delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action; delete manifest.commands._execute_sidebar_action;
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest; return manifest;
}); });
} }
@ -114,9 +88,6 @@ function distEdge() {
delete manifest.applications; delete manifest.applications;
delete manifest.sidebar_action; delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action; delete manifest.commands._execute_sidebar_action;
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest; return manifest;
}); });
} }
@ -237,9 +208,6 @@ async function safariCopyBuild(source, dest) {
delete manifest.commands._execute_sidebar_action; delete manifest.commands._execute_sidebar_action;
delete manifest.optional_permissions; delete manifest.optional_permissions;
manifest.permissions.push("nativeMessaging"); manifest.permissions.push("nativeMessaging");
if (betaBuild) {
manifest = applyBetaLabels(manifest);
}
return manifest; return manifest;
}), }),
), ),
@ -254,30 +222,6 @@ function stdOutProc(proc) {
proc.stderr.on("data", (data) => console.error(data.toString())); proc.stderr.on("data", (data) => console.error(data.toString()));
} }
function applyBetaLabels(manifest) {
manifest.name = "Bitwarden Password Manager BETA";
manifest.short_name = "Bitwarden BETA";
manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN.";
if (process.env.GITHUB_RUN_ID) {
const existingVersionParts = manifest.version.split("."); // 3 parts expected 2024.4.0
// GITHUB_RUN_ID is a number like: 8853654662
// which will convert to [ 4024, 3206 ]
// and a single incremented id of 8853654663 will become [ 4024, 3207 ]
const runIdParts = numToUint16s(parseInt(process.env.GITHUB_RUN_ID));
// Only use the first 2 parts from the given version number and base the other 2 numbers from the GITHUB_RUN_ID
// Example: 2024.4.4024.3206
const betaVersion = `${existingVersionParts[0]}.${existingVersionParts[1]}.${runIdParts[0]}.${runIdParts[1]}`;
manifest.version_name = `${betaVersion} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`;
manifest.version = betaVersion;
} else {
manifest.version = `${manifest.version}.0`;
}
return manifest;
}
exports["dist:firefox"] = distFirefox; exports["dist:firefox"] = distFirefox;
exports["dist:chrome"] = distChrome; exports["dist:chrome"] = distChrome;
exports["dist:opera"] = distOpera; exports["dist:opera"] = distOpera;

View File

@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2024.10.1", "version": "2024.11.0",
"scripts": { "scripts": {
"build": "cross-env MANIFEST_VERSION=3 webpack", "build": "cross-env MANIFEST_VERSION=3 webpack",
"build:mv2": "webpack", "build:mv2": "webpack",
@ -10,12 +10,9 @@
"build:watch:safari": "cross-env MANIFEST_VERSION=3 BROWSER=safari webpack --watch", "build:watch:safari": "cross-env MANIFEST_VERSION=3 BROWSER=safari webpack --watch",
"build:watch:mv2": "webpack --watch", "build:watch:mv2": "webpack --watch",
"build:prod": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" webpack", "build:prod": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" webpack",
"build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack",
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"dist": "npm run build:prod && gulp dist", "dist": "npm run build:prod && gulp dist",
"dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist",
"dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist", "dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist",
"dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist",
"dist:chrome": "npm run build:prod && gulp dist:chrome", "dist:chrome": "npm run build:prod && gulp dist:chrome",
"dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome",
"dist:firefox": "npm run build:prod && gulp dist:firefox", "dist:firefox": "npm run build:prod && gulp dist:firefox",

View File

@ -2878,7 +2878,7 @@
"message": "Luo sähköpostiosoite" "message": "Luo sähköpostiosoite"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Arvon tulee olla väliltä $MIN$ - $MAX$", "message": "Arvon tulee olla väliltä $MIN$$MAX$",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {

View File

@ -3,7 +3,7 @@
"message": "Bitwarden" "message": "Bitwarden"
}, },
"extName": { "extName": {
"message": "Bitwarden Password Manager", "message": "Bitwarden passordbehandler",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)" "description": "Extension name, MUST be less than 40 characters (Safari restriction)"
}, },
"extDesc": { "extDesc": {
@ -29,7 +29,7 @@
"message": "Use single sign-on" "message": "Use single sign-on"
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "Velkommen tilbake"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "Set a strong password" "message": "Set a strong password"
@ -168,7 +168,7 @@
"message": "Copy notes" "message": "Copy notes"
}, },
"fill": { "fill": {
"message": "Fill", "message": "Fyll",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." "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": { "autoFill": {
@ -357,10 +357,10 @@
"message": "Rediger mappen" "message": "Rediger mappen"
}, },
"newFolder": { "newFolder": {
"message": "New folder" "message": "Ny mappe"
}, },
"folderName": { "folderName": {
"message": "Folder name" "message": "Mappenavn"
}, },
"folderHintText": { "folderHintText": {
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
@ -458,7 +458,7 @@
"description": "deprecated. Use specialCharactersLabel instead." "description": "deprecated. Use specialCharactersLabel instead."
}, },
"include": { "include": {
"message": "Include", "message": "Inkluder",
"description": "Card header for password generator include block" "description": "Card header for password generator include block"
}, },
"uppercaseDescription": { "uppercaseDescription": {
@ -466,7 +466,7 @@
"description": "Tooltip for the password generator uppercase character checkbox" "description": "Tooltip for the password generator uppercase character checkbox"
}, },
"uppercaseLabel": { "uppercaseLabel": {
"message": "A-Z", "message": "A-Å",
"description": "Label for the password generator uppercase character checkbox" "description": "Label for the password generator uppercase character checkbox"
}, },
"lowercaseDescription": { "lowercaseDescription": {
@ -478,7 +478,7 @@
"description": "Label for the password generator lowercase character checkbox" "description": "Label for the password generator lowercase character checkbox"
}, },
"numbersDescription": { "numbersDescription": {
"message": "Include numbers", "message": "Inkluder tall",
"description": "Full description for the password generator numbers checkbox" "description": "Full description for the password generator numbers checkbox"
}, },
"numbersLabel": { "numbersLabel": {
@ -627,7 +627,7 @@
"message": "Vault timeout" "message": "Vault timeout"
}, },
"otherOptions": { "otherOptions": {
"message": "Other options" "message": "Andre valg"
}, },
"rateExtension": { "rateExtension": {
"message": "Gi denne utvidelsen en vurdering" "message": "Gi denne utvidelsen en vurdering"
@ -676,7 +676,7 @@
"message": "Tidsavbrudd i hvelvet" "message": "Tidsavbrudd i hvelvet"
}, },
"vaultTimeout1": { "vaultTimeout1": {
"message": "Timeout" "message": "Tidsavbrudd"
}, },
"lockNow": { "lockNow": {
"message": "Lås nå" "message": "Lås nå"
@ -730,10 +730,10 @@
"message": "Sikkerhet" "message": "Sikkerhet"
}, },
"confirmMasterPassword": { "confirmMasterPassword": {
"message": "Confirm master password" "message": "Bekreft hovedpassord"
}, },
"masterPassword": { "masterPassword": {
"message": "Master password" "message": "Hovedpassord"
}, },
"masterPassImportant": { "masterPassImportant": {
"message": "Your master password cannot be recovered if you forget it!" "message": "Your master password cannot be recovered if you forget it!"
@ -843,7 +843,7 @@
"message": "Din innloggingsøkt har utløpt." "message": "Din innloggingsøkt har utløpt."
}, },
"logIn": { "logIn": {
"message": "Log in" "message": "Logg inn"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "Log in to Bitwarden"
@ -928,7 +928,7 @@
"message": "Ny URI" "message": "Ny URI"
}, },
"addDomain": { "addDomain": {
"message": "Add domain", "message": "Legg til domene",
"description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context."
}, },
"addedItem": { "addedItem": {
@ -1092,7 +1092,7 @@
"message": "This file export will be password protected and require the file password to decrypt." "message": "This file export will be password protected and require the file password to decrypt."
}, },
"filePassword": { "filePassword": {
"message": "File password" "message": "Filpassord"
}, },
"exportPasswordDescription": { "exportPasswordDescription": {
"message": "This password will be used to export and import this file" "message": "This password will be used to export and import this file"
@ -1917,10 +1917,10 @@
"message": "Det er ingen passord å liste opp." "message": "Det er ingen passord å liste opp."
}, },
"clearHistory": { "clearHistory": {
"message": "Clear history" "message": "Tøm historikk"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "Ingenting å vise"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "You haven't generated anything recently" "message": "You haven't generated anything recently"
@ -1984,10 +1984,10 @@
"message": "Lås opp med PIN-kode" "message": "Lås opp med PIN-kode"
}, },
"setYourPinTitle": { "setYourPinTitle": {
"message": "Set PIN" "message": "Velg PIN"
}, },
"setYourPinButton": { "setYourPinButton": {
"message": "Set PIN" "message": "Velg PIN"
}, },
"setYourPinCode": { "setYourPinCode": {
"message": "Angi PIN-koden din for å låse opp Bitwarden. PIN-innstillingene tilbakestilles hvis du logger deg helt ut av programmet." "message": "Angi PIN-koden din for å låse opp Bitwarden. PIN-innstillingene tilbakestilles hvis du logger deg helt ut av programmet."
@ -2041,7 +2041,7 @@
"message": "Username generator" "message": "Username generator"
}, },
"useThisPassword": { "useThisPassword": {
"message": "Use this password" "message": "Bruk dette passordet"
}, },
"useThisUsername": { "useThisUsername": {
"message": "Use this username" "message": "Use this username"
@ -2186,7 +2186,7 @@
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox."
}, },
"unsubscribe": { "unsubscribe": {
"message": "Unsubscribe" "message": "Avslutt abonnement"
}, },
"atAnyTime": { "atAnyTime": {
"message": "at any time." "message": "at any time."
@ -2195,7 +2195,7 @@
"message": "By continuing, you agree to the" "message": "By continuing, you agree to the"
}, },
"and": { "and": {
"message": "and" "message": "og"
}, },
"acceptPolicies": { "acceptPolicies": {
"message": "Ved å merke av denne boksen sier du deg enig i følgende:" "message": "Ved å merke av denne boksen sier du deg enig i følgende:"
@ -2315,7 +2315,7 @@
"message": "An organization policy has blocked importing items into your individual vault." "message": "An organization policy has blocked importing items into your individual vault."
}, },
"domainsTitle": { "domainsTitle": {
"message": "Domains", "message": "Domener",
"description": "A category title describing the concept of web domains" "description": "A category title describing the concept of web domains"
}, },
"excludedDomains": { "excludedDomains": {
@ -2411,7 +2411,7 @@
"message": "Passord beskyttet" "message": "Passord beskyttet"
}, },
"copyLink": { "copyLink": {
"message": "Copy link" "message": "Kopier lenke"
}, },
"copySendLink": { "copySendLink": {
"message": "Kopier Send-lenke", "message": "Kopier Send-lenke",
@ -2668,7 +2668,7 @@
"message": "E-postbekreftelse kreves" "message": "E-postbekreftelse kreves"
}, },
"emailVerifiedV2": { "emailVerifiedV2": {
"message": "Email verified" "message": "E-post bekreftet"
}, },
"emailVerificationRequiredDesc": { "emailVerificationRequiredDesc": {
"message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet." "message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet."
@ -3247,10 +3247,10 @@
"message": "Uncheck if using a public device" "message": "Uncheck if using a public device"
}, },
"approveFromYourOtherDevice": { "approveFromYourOtherDevice": {
"message": "Approve from your other device" "message": "Godkjenn fra en av dine andre enheter"
}, },
"requestAdminApproval": { "requestAdminApproval": {
"message": "Request admin approval" "message": "Be om administratorgodkjennelse"
}, },
"approveWithMasterPassword": { "approveWithMasterPassword": {
"message": "Godkjenn med hovedpassord" "message": "Godkjenn med hovedpassord"
@ -3274,7 +3274,7 @@
"message": "No email?" "message": "No email?"
}, },
"goBack": { "goBack": {
"message": "Go back" "message": "Gå tilbake"
}, },
"toEditYourEmailAddress": { "toEditYourEmailAddress": {
"message": "to edit your email address." "message": "to edit your email address."
@ -3290,7 +3290,7 @@
"message": "Generelt" "message": "Generelt"
}, },
"display": { "display": {
"message": "Display" "message": "Vis"
}, },
"accountSuccessfullyCreated": { "accountSuccessfullyCreated": {
"message": "Account successfully created!" "message": "Account successfully created!"
@ -3416,7 +3416,7 @@
"message": "— Skriv for å filtrere —" "message": "— Skriv for å filtrere —"
}, },
"multiSelectLoading": { "multiSelectLoading": {
"message": "Retrieving options..." "message": "Innhenter alternativer..."
}, },
"multiSelectNotFound": { "multiSelectNotFound": {
"message": "Ingen gjenstander funnet" "message": "Ingen gjenstander funnet"
@ -3542,7 +3542,7 @@
"description": "Screen reader text (aria-label) for new item button in overlay" "description": "Screen reader text (aria-label) for new item button in overlay"
}, },
"newLogin": { "newLogin": {
"message": "New login", "message": "Ny innlogging",
"description": "Button text to display within inline menu when there are no matching items on a login field" "description": "Button text to display within inline menu when there are no matching items on a login field"
}, },
"addNewLoginItemAria": { "addNewLoginItemAria": {
@ -3558,7 +3558,7 @@
"description": "Screen reader text (aria-label) for new card button within inline menu" "description": "Screen reader text (aria-label) for new card button within inline menu"
}, },
"newIdentity": { "newIdentity": {
"message": "New identity", "message": "Ny identitet",
"description": "Button text to display within inline menu when there are no matching items on an identity field" "description": "Button text to display within inline menu when there are no matching items on an identity field"
}, },
"addNewIdentityItemAria": { "addNewIdentityItemAria": {
@ -3592,7 +3592,7 @@
"message": "Beskrivelse" "message": "Beskrivelse"
}, },
"importSuccess": { "importSuccess": {
"message": "Data successfully imported" "message": "Dataene ble vellykket importert"
}, },
"importSuccessNumberOfItems": { "importSuccessNumberOfItems": {
"message": "$AMOUNT$ gjenstander totalt ble importert.", "message": "$AMOUNT$ gjenstander totalt ble importert.",
@ -3682,7 +3682,7 @@
"message": "Invalid file password, please use the password you entered when you created the export file." "message": "Invalid file password, please use the password you entered when you created the export file."
}, },
"destination": { "destination": {
"message": "Destination" "message": "Destinasjon"
}, },
"learnAboutImportOptions": { "learnAboutImportOptions": {
"message": "Lær mer om importalternativene dine" "message": "Lær mer om importalternativene dine"
@ -3719,7 +3719,7 @@
"message": "Ingen fil er valgt" "message": "Ingen fil er valgt"
}, },
"orCopyPasteFileContents": { "orCopyPasteFileContents": {
"message": "or copy/paste the import file contents" "message": "eller kopier/lim inn importfilens innhold"
}, },
"instructionsFor": { "instructionsFor": {
"message": "$NAME$-instruksjoner", "message": "$NAME$-instruksjoner",
@ -3810,7 +3810,7 @@
"message": "Multifaktorautentisering ble avbrutt" "message": "Multifaktorautentisering ble avbrutt"
}, },
"noLastPassDataFound": { "noLastPassDataFound": {
"message": "No LastPass data found" "message": "Ingen LastPass-data ble funnet"
}, },
"incorrectUsernameOrPassword": { "incorrectUsernameOrPassword": {
"message": "Feil brukernavn eller passord" "message": "Feil brukernavn eller passord"
@ -3837,7 +3837,7 @@
"message": "Importerer kontoen din…" "message": "Importerer kontoen din…"
}, },
"lastPassMFARequired": { "lastPassMFARequired": {
"message": "LastPass multifactor authentication required" "message": "LastPass-multifaktorautentisering kreves"
}, },
"lastPassMFADesc": { "lastPassMFADesc": {
"message": "Enter your one-time passcode from your authentication app" "message": "Enter your one-time passcode from your authentication app"
@ -4003,7 +4003,7 @@
"description": "Notification message for when saving credentials has failed." "description": "Notification message for when saving credentials has failed."
}, },
"success": { "success": {
"message": "Success" "message": "Suksess"
}, },
"removePasskey": { "removePasskey": {
"message": "Remove passkey" "message": "Remove passkey"
@ -4105,13 +4105,13 @@
"message": "Admin Console" "message": "Admin Console"
}, },
"accountSecurity": { "accountSecurity": {
"message": "Account security" "message": "Kontosikkerhet"
}, },
"notifications": { "notifications": {
"message": "Notifications" "message": "Varsler"
}, },
"appearance": { "appearance": {
"message": "Appearance" "message": "Utseende"
}, },
"errorAssigningTargetCollection": { "errorAssigningTargetCollection": {
"message": "Error assigning target collection." "message": "Error assigning target collection."
@ -4140,7 +4140,7 @@
} }
}, },
"new": { "new": {
"message": "New" "message": "Ny"
}, },
"removeItem": { "removeItem": {
"message": "Remove $NAME$", "message": "Remove $NAME$",
@ -4174,17 +4174,17 @@
"message": "Organization is deactivated" "message": "Organization is deactivated"
}, },
"owner": { "owner": {
"message": "Owner" "message": "Eier"
}, },
"selfOwnershipLabel": { "selfOwnershipLabel": {
"message": "You", "message": "Du",
"description": "Used as a label to indicate that the user is the owner of an item." "description": "Used as a label to indicate that the user is the owner of an item."
}, },
"contactYourOrgAdmin": { "contactYourOrgAdmin": {
"message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance."
}, },
"additionalInformation": { "additionalInformation": {
"message": "Additional information" "message": "Tilleggsinformasjon"
}, },
"itemHistory": { "itemHistory": {
"message": "Item history" "message": "Item history"
@ -4196,13 +4196,13 @@
"message": "Owner: You" "message": "Owner: You"
}, },
"linked": { "linked": {
"message": "Linked" "message": "Tilknyttet"
}, },
"copySuccessful": { "copySuccessful": {
"message": "Copy Successful" "message": "Copy Successful"
}, },
"upload": { "upload": {
"message": "Upload" "message": "Last opp"
}, },
"addAttachment": { "addAttachment": {
"message": "Add attachment" "message": "Add attachment"
@ -4238,7 +4238,7 @@
"message": "Free organizations cannot use attachments" "message": "Free organizations cannot use attachments"
}, },
"filters": { "filters": {
"message": "Filters" "message": "Filtre"
}, },
"personalDetails": { "personalDetails": {
"message": "Personal details" "message": "Personal details"
@ -4247,7 +4247,7 @@
"message": "Identification" "message": "Identification"
}, },
"contactInfo": { "contactInfo": {
"message": "Contact info" "message": "Kontaktinfo"
}, },
"downloadAttachment": { "downloadAttachment": {
"message": "Download - $ITEMNAME$", "message": "Download - $ITEMNAME$",
@ -4263,7 +4263,7 @@
"description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher."
}, },
"loginCredentials": { "loginCredentials": {
"message": "Login credentials" "message": "Legitimasjoner for innlogging"
}, },
"authenticatorKey": { "authenticatorKey": {
"message": "Authenticator key" "message": "Authenticator key"
@ -4288,10 +4288,10 @@
"message": "Website added" "message": "Website added"
}, },
"addWebsite": { "addWebsite": {
"message": "Add website" "message": "Legg til nettsted"
}, },
"deleteWebsite": { "deleteWebsite": {
"message": "Delete website" "message": "Slett nettsted"
}, },
"defaultLabel": { "defaultLabel": {
"message": "Default ($VALUE$)", "message": "Default ($VALUE$)",
@ -4343,16 +4343,16 @@
} }
}, },
"enableAnimations": { "enableAnimations": {
"message": "Enable animations" "message": "Aktiver animasjoner"
}, },
"showAnimations": { "showAnimations": {
"message": "Show animations" "message": "Show animations"
}, },
"addAccount": { "addAccount": {
"message": "Add account" "message": "Legg til konto"
}, },
"loading": { "loading": {
"message": "Loading" "message": "Laster"
}, },
"data": { "data": {
"message": "Data" "message": "Data"
@ -4370,7 +4370,7 @@
"description": "ARIA label for the inline menu button that logs in with a passkey." "description": "ARIA label for the inline menu button that logs in with a passkey."
}, },
"assign": { "assign": {
"message": "Assign" "message": "Knytt"
}, },
"bulkCollectionAssignmentDialogDescriptionSingular": { "bulkCollectionAssignmentDialogDescriptionSingular": {
"message": "Only organization members with access to these collections will be able to see the item." "message": "Only organization members with access to these collections will be able to see the item."
@ -4394,7 +4394,7 @@
"message": "Add field" "message": "Add field"
}, },
"add": { "add": {
"message": "Add" "message": "Legg til"
}, },
"fieldType": { "fieldType": {
"message": "Field type" "message": "Field type"
@ -4418,7 +4418,7 @@
"message": "Enter the the field's html id, name, aria-label, or placeholder." "message": "Enter the the field's html id, name, aria-label, or placeholder."
}, },
"editField": { "editField": {
"message": "Edit field" "message": "Rediger felt"
}, },
"editFieldLabel": { "editFieldLabel": {
"message": "Edit $LABEL$", "message": "Edit $LABEL$",
@ -4588,13 +4588,13 @@
"message": "Show number of login autofill suggestions on extension icon" "message": "Show number of login autofill suggestions on extension icon"
}, },
"systemDefault": { "systemDefault": {
"message": "System default" "message": "Systemforvalg"
}, },
"enterprisePolicyRequirementsApplied": { "enterprisePolicyRequirementsApplied": {
"message": "Enterprise policy requirements have been applied to this setting" "message": "Enterprise policy requirements have been applied to this setting"
}, },
"retry": { "retry": {
"message": "Retry" "message": "Prøv igjen"
}, },
"vaultCustomTimeoutMinimum": { "vaultCustomTimeoutMinimum": {
"message": "Minimum custom timeout is 1 minute." "message": "Minimum custom timeout is 1 minute."
@ -4624,16 +4624,16 @@
"message": "Items that have been in trash more than 30 days will automatically be deleted" "message": "Items that have been in trash more than 30 days will automatically be deleted"
}, },
"restore": { "restore": {
"message": "Restore" "message": "Gjenopprett"
}, },
"deleteForever": { "deleteForever": {
"message": "Delete forever" "message": "Slett for alltid"
}, },
"noEditPermissions": { "noEditPermissions": {
"message": "You don't have permission to edit this item" "message": "You don't have permission to edit this item"
}, },
"authenticating": { "authenticating": {
"message": "Authenticating" "message": "Autentiserer"
}, },
"fillGeneratedPassword": { "fillGeneratedPassword": {
"message": "Fill generated password", "message": "Fill generated password",
@ -4648,7 +4648,7 @@
"description": "Confirmation message for saving a login to Bitwarden" "description": "Confirmation message for saving a login to Bitwarden"
}, },
"spaceCharacterDescriptor": { "spaceCharacterDescriptor": {
"message": "Space", "message": "Mellomrom",
"description": "Represents the space key in screen reader content as a readable word" "description": "Represents the space key in screen reader content as a readable word"
}, },
"tildeCharacterDescriptor": { "tildeCharacterDescriptor": {
@ -4656,15 +4656,15 @@
"description": "Represents the ~ key in screen reader content as a readable word" "description": "Represents the ~ key in screen reader content as a readable word"
}, },
"backtickCharacterDescriptor": { "backtickCharacterDescriptor": {
"message": "Backtick", "message": "Baklengs apostrof",
"description": "Represents the ` key in screen reader content as a readable word" "description": "Represents the ` key in screen reader content as a readable word"
}, },
"exclamationCharacterDescriptor": { "exclamationCharacterDescriptor": {
"message": "Exclamation mark", "message": "Utropstegn",
"description": "Represents the ! key in screen reader content as a readable word" "description": "Represents the ! key in screen reader content as a readable word"
}, },
"atSignCharacterDescriptor": { "atSignCharacterDescriptor": {
"message": "At sign", "message": "Alfakrøll",
"description": "Represents the @ key in screen reader content as a readable word" "description": "Represents the @ key in screen reader content as a readable word"
}, },
"hashSignCharacterDescriptor": { "hashSignCharacterDescriptor": {
@ -4672,7 +4672,7 @@
"description": "Represents the # key in screen reader content as a readable word" "description": "Represents the # key in screen reader content as a readable word"
}, },
"dollarSignCharacterDescriptor": { "dollarSignCharacterDescriptor": {
"message": "Dollar sign", "message": "Dollartegn",
"description": "Represents the $ key in screen reader content as a readable word" "description": "Represents the $ key in screen reader content as a readable word"
}, },
"percentSignCharacterDescriptor": { "percentSignCharacterDescriptor": {
@ -4684,7 +4684,7 @@
"description": "Represents the ^ key in screen reader content as a readable word" "description": "Represents the ^ key in screen reader content as a readable word"
}, },
"ampersandCharacterDescriptor": { "ampersandCharacterDescriptor": {
"message": "Ampersand", "message": "Prosenttegn",
"description": "Represents the & key in screen reader content as a readable word" "description": "Represents the & key in screen reader content as a readable word"
}, },
"asteriskCharacterDescriptor": { "asteriskCharacterDescriptor": {
@ -4700,7 +4700,7 @@
"description": "Represents the ) key in screen reader content as a readable word" "description": "Represents the ) key in screen reader content as a readable word"
}, },
"hyphenCharacterDescriptor": { "hyphenCharacterDescriptor": {
"message": "Underscore", "message": "Understrek",
"description": "Represents the _ key in screen reader content as a readable word" "description": "Represents the _ key in screen reader content as a readable word"
}, },
"underscoreCharacterDescriptor": { "underscoreCharacterDescriptor": {
@ -4740,11 +4740,11 @@
"description": "Represents the back slash key in screen reader content as a readable word" "description": "Represents the back slash key in screen reader content as a readable word"
}, },
"colonCharacterDescriptor": { "colonCharacterDescriptor": {
"message": "Colon", "message": "Kolon",
"description": "Represents the : key in screen reader content as a readable word" "description": "Represents the : key in screen reader content as a readable word"
}, },
"semicolonCharacterDescriptor": { "semicolonCharacterDescriptor": {
"message": "Semicolon", "message": "Semikolon",
"description": "Represents the ; key in screen reader content as a readable word" "description": "Represents the ; key in screen reader content as a readable word"
}, },
"doubleQuoteCharacterDescriptor": { "doubleQuoteCharacterDescriptor": {
@ -4756,7 +4756,7 @@
"description": "Represents the ' key in screen reader content as a readable word" "description": "Represents the ' key in screen reader content as a readable word"
}, },
"lessThanCharacterDescriptor": { "lessThanCharacterDescriptor": {
"message": "Less than", "message": "Mindre enn",
"description": "Represents the < key in screen reader content as a readable word" "description": "Represents the < key in screen reader content as a readable word"
}, },
"greaterThanCharacterDescriptor": { "greaterThanCharacterDescriptor": {
@ -4764,7 +4764,7 @@
"description": "Represents the > key in screen reader content as a readable word" "description": "Represents the > key in screen reader content as a readable word"
}, },
"commaCharacterDescriptor": { "commaCharacterDescriptor": {
"message": "Comma", "message": "Komma",
"description": "Represents the , key in screen reader content as a readable word" "description": "Represents the , key in screen reader content as a readable word"
}, },
"periodCharacterDescriptor": { "periodCharacterDescriptor": {
@ -4772,7 +4772,7 @@
"description": "Represents the . key in screen reader content as a readable word" "description": "Represents the . key in screen reader content as a readable word"
}, },
"questionCharacterDescriptor": { "questionCharacterDescriptor": {
"message": "Question mark", "message": "Spørsmålstegn",
"description": "Represents the ? key in screen reader content as a readable word" "description": "Represents the ? key in screen reader content as a readable word"
}, },
"forwardSlashCharacterDescriptor": { "forwardSlashCharacterDescriptor": {
@ -4780,10 +4780,10 @@
"description": "Represents the / key in screen reader content as a readable word" "description": "Represents the / key in screen reader content as a readable word"
}, },
"lowercaseAriaLabel": { "lowercaseAriaLabel": {
"message": "Lowercase" "message": "Små bokstaver"
}, },
"uppercaseAriaLabel": { "uppercaseAriaLabel": {
"message": "Uppercase" "message": "Store bokstaver"
}, },
"generatedPassword": { "generatedPassword": {
"message": "Generated password" "message": "Generated password"

View File

@ -20,16 +20,16 @@
"message": "Креирај налог" "message": "Креирај налог"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "Нови сте у Bitwarden-у?"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "Пријавите се са приступним кључем"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "Употребити једнократну пријаву"
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "Добродошли назад"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "Поставите јаку лозинку" "message": "Поставите јаку лозинку"
@ -120,7 +120,7 @@
"message": "Копирај лозинку" "message": "Копирај лозинку"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase" "message": "Копирај приступну фразу"
}, },
"copyNote": { "copyNote": {
"message": "Копирај белешку" "message": "Копирај белешку"
@ -168,7 +168,7 @@
"message": "Копирати белешке" "message": "Копирати белешке"
}, },
"fill": { "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." "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": { "autoFill": {
@ -427,7 +427,7 @@
"message": "Генерисање лозинке" "message": "Генерисање лозинке"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "Генеришите приступну фразу"
}, },
"regeneratePassword": { "regeneratePassword": {
"message": "Поново генериши лозинку" "message": "Поново генериши лозинку"
@ -591,7 +591,7 @@
"message": "Покрените веб локацију" "message": "Покрените веб локацију"
}, },
"launchWebsiteName": { "launchWebsiteName": {
"message": "Launch website $ITEMNAME$", "message": "Покренути сајт $ITEMNAME$",
"placeholders": { "placeholders": {
"itemname": { "itemname": {
"content": "$1", "content": "$1",
@ -846,7 +846,7 @@
"message": "Пријави се" "message": "Пријави се"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "Пријавите се на Bitwarden"
}, },
"restartRegistration": { "restartRegistration": {
"message": "Поново покрените регистрацију" "message": "Поново покрените регистрацију"
@ -1424,7 +1424,7 @@
"message": "УРЛ Сервера" "message": "УРЛ Сервера"
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "УРЛ сервера који се самостално хостује",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@ -1795,13 +1795,13 @@
"message": "Историја Лозинке" "message": "Историја Лозинке"
}, },
"generatorHistory": { "generatorHistory": {
"message": "Generator history" "message": "Генератор историје"
}, },
"clearGeneratorHistoryTitle": { "clearGeneratorHistoryTitle": {
"message": "Clear generator history" "message": "Испразнити генератор историје"
}, },
"cleargGeneratorHistoryDescription": { "cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" "message": "Ако наставите, сви уноси ће бити трајно избрисани из генератора историје. Да ли сте сигурни да желите да наставите?"
}, },
"back": { "back": {
"message": "Назад" "message": "Назад"
@ -1920,10 +1920,10 @@
"message": "Обриши историју" "message": "Обриши историју"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "Нема шта да се прикаже"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "You haven't generated anything recently" "message": "Недавно нисте ништа генерисали"
}, },
"remove": { "remove": {
"message": "Уклони" "message": "Уклони"
@ -2183,7 +2183,7 @@
"message": "Ваша нова главна лозинка не испуњава захтеве смерница." "message": "Ваша нова главна лозинка не испуњава захтеве смерница."
}, },
"receiveMarketingEmailsV2": { "receiveMarketingEmailsV2": {
"message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." "message": "Добијајте савете, најаве и могућности истраживања од Bitwarden-а у пријемном сандучету."
}, },
"unsubscribe": { "unsubscribe": {
"message": "Одјави се" "message": "Одјави се"
@ -2512,7 +2512,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendPasswordDescV3": { "sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.", "message": "Додајте опционалну лозинку за примаоце да приступе овом Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendNotesDesc": { "sendNotesDesc": {
@ -2875,10 +2875,10 @@
"message": "Генериши име" "message": "Генериши име"
}, },
"generateEmail": { "generateEmail": {
"message": "Generate email" "message": "Генеришите имејл"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$", "message": "Вредност мора бити између $MIN$ и $MAX$",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@ -2932,11 +2932,11 @@
"message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања." "message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања."
}, },
"forwarderDomainName": { "forwarderDomainName": {
"message": "Email domain", "message": "Домен имејла",
"description": "Labels the domain name email forwarder service option" "description": "Labels the domain name email forwarder service option"
}, },
"forwarderDomainNameHint": { "forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service", "message": "Изаберите домен који подржава изабрана услуга",
"description": "Guidance provided for email forwarding services that support multiple email domains." "description": "Guidance provided for email forwarding services that support multiple email domains."
}, },
"forwarderError": { "forwarderError": {
@ -3664,7 +3664,7 @@
"message": "Искачући додатак да бисте довршили пријаву." "message": "Искачући додатак да бисте довршили пријаву."
}, },
"popoutExtension": { "popoutExtension": {
"message": "Popout extension" "message": "Искачући додатак"
}, },
"launchDuo": { "launchDuo": {
"message": "Покренути DUO" "message": "Покренути DUO"
@ -3744,25 +3744,25 @@
"message": "Подаци из сефа су извезени" "message": "Подаци из сефа су извезени"
}, },
"typePasskey": { "typePasskey": {
"message": "Приступачни кључ" "message": "Приступни кључ"
}, },
"accessing": { "accessing": {
"message": "Приступ" "message": "Приступ"
}, },
"passkeyNotCopied": { "passkeyNotCopied": {
"message": "Приступачни кључ неће бити копиран" "message": "Приступни кључ неће бити копиран"
}, },
"passkeyNotCopiedAlert": { "passkeyNotCopiedAlert": {
"message": "Приступачни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?" "message": "Приступни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?"
}, },
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
"message": "Верификацију захтева сајт који покреће. Ова функција још увек није имплементирана за налоге без главне лозинке." "message": "Верификацију захтева сајт који покреће. Ова функција још увек није имплементирана за налоге без главне лозинке."
}, },
"logInWithPasskeyQuestion": { "logInWithPasskeyQuestion": {
"message": "Пријавите се са приступачним кључем?" "message": "Пријавите се са приступним кључем?"
}, },
"passkeyAlreadyExists": { "passkeyAlreadyExists": {
"message": "За ову апликацију већ постоји приступачни кључ." "message": "За ову апликацију већ постоји приступни кључ."
}, },
"noPasskeysFoundForThisApplication": { "noPasskeysFoundForThisApplication": {
"message": "Нису пронађени приступни кључеви за ову апликацију." "message": "Нису пронађени приступни кључеви за ову апликацију."
@ -3792,7 +3792,7 @@
"message": "Изаберите приступни кључ за пријаву" "message": "Изаберите приступни кључ за пријаву"
}, },
"passkeyItem": { "passkeyItem": {
"message": "Ставка приступачног кључа" "message": "Ставка приступног кључа"
}, },
"overwritePasskey": { "overwritePasskey": {
"message": "Заменити приступни кључ?" "message": "Заменити приступни кључ?"
@ -3910,7 +3910,7 @@
"message": "сервер" "message": "сервер"
}, },
"hostedAt": { "hostedAt": {
"message": "hosted at" "message": "хостиран на"
}, },
"useDeviceOrHardwareKey": { "useDeviceOrHardwareKey": {
"message": "Користите свој уређај или хардверски кључ" "message": "Користите свој уређај или хардверски кључ"
@ -4006,10 +4006,10 @@
"message": "Успех" "message": "Успех"
}, },
"removePasskey": { "removePasskey": {
"message": "Уклонити приступачни кључ" "message": "Уклонити приступни кључ"
}, },
"passkeyRemoved": { "passkeyRemoved": {
"message": "Приступачни кључ је уклоњен" "message": "Приступни кључ је уклоњен"
}, },
"autofillSuggestions": { "autofillSuggestions": {
"message": "Предлози за ауто-попуњавање" "message": "Предлози за ауто-попуњавање"
@ -4358,7 +4358,7 @@
"message": "Подаци" "message": "Подаци"
}, },
"passkeys": { "passkeys": {
"message": "Приступачни кључеви", "message": "Приступни кључеви",
"description": "A section header for a list of passkeys." "description": "A section header for a list of passkeys."
}, },
"passwords": { "passwords": {
@ -4366,7 +4366,7 @@
"description": "A section header for a list of passwords." "description": "A section header for a list of passwords."
}, },
"logInWithPasskeyAriaLabel": { "logInWithPasskeyAriaLabel": {
"message": "Пријавите се са приступачним кључем", "message": "Пријавите се са приступним кључем",
"description": "ARIA label for the inline menu button that logs in with a passkey." "description": "ARIA label for the inline menu button that logs in with a passkey."
}, },
"assign": { "assign": {
@ -4564,13 +4564,13 @@
"message": "Смештај ставке" "message": "Смештај ставке"
}, },
"fileSend": { "fileSend": {
"message": "File Send" "message": "Датотека „Send“"
}, },
"fileSends": { "fileSends": {
"message": "Датотека „Send“" "message": "Датотека „Send“"
}, },
"textSend": { "textSend": {
"message": "Text Send" "message": "Текст „Send“"
}, },
"textSends": { "textSends": {
"message": "Текст „Send“" "message": "Текст „Send“"
@ -4603,7 +4603,7 @@
"message": "Додатни садржај је доступан" "message": "Додатни садржај је доступан"
}, },
"fileSavedToDevice": { "fileSavedToDevice": {
"message": "File saved to device. Manage from your device downloads." "message": "Датотека је сачувана на уређају. Управљајте преузимањима са свог уређаја."
}, },
"showCharacterCount": { "showCharacterCount": {
"message": "Прикажи бројање слова" "message": "Прикажи бројање слова"
@ -4636,23 +4636,23 @@
"message": "Аутентификација" "message": "Аутентификација"
}, },
"fillGeneratedPassword": { "fillGeneratedPassword": {
"message": "Fill generated password", "message": "Попуните генерисану лозинку",
"description": "Heading for the password generator within the inline menu" "description": "Heading for the password generator within the inline menu"
}, },
"passwordRegenerated": { "passwordRegenerated": {
"message": "Password regenerated", "message": "Лозинка поново генерисана",
"description": "Notification message for when a password has been regenerated" "description": "Notification message for when a password has been regenerated"
}, },
"saveLoginToBitwarden": { "saveLoginToBitwarden": {
"message": "Save login to Bitwarden?", "message": "Сачувати пријаву на Bitwarden?",
"description": "Confirmation message for saving a login to Bitwarden" "description": "Confirmation message for saving a login to Bitwarden"
}, },
"spaceCharacterDescriptor": { "spaceCharacterDescriptor": {
"message": "Space", "message": "Простор",
"description": "Represents the space key in screen reader content as a readable word" "description": "Represents the space key in screen reader content as a readable word"
}, },
"tildeCharacterDescriptor": { "tildeCharacterDescriptor": {
"message": "Tilde", "message": "Тилда",
"description": "Represents the ~ key in screen reader content as a readable word" "description": "Represents the ~ key in screen reader content as a readable word"
}, },
"backtickCharacterDescriptor": { "backtickCharacterDescriptor": {
@ -4660,55 +4660,55 @@
"description": "Represents the ` key in screen reader content as a readable word" "description": "Represents the ` key in screen reader content as a readable word"
}, },
"exclamationCharacterDescriptor": { "exclamationCharacterDescriptor": {
"message": "Exclamation mark", "message": "Узвичник",
"description": "Represents the ! key in screen reader content as a readable word" "description": "Represents the ! key in screen reader content as a readable word"
}, },
"atSignCharacterDescriptor": { "atSignCharacterDescriptor": {
"message": "At sign", "message": "Знак „ет“",
"description": "Represents the @ key in screen reader content as a readable word" "description": "Represents the @ key in screen reader content as a readable word"
}, },
"hashSignCharacterDescriptor": { "hashSignCharacterDescriptor": {
"message": "Hash sign", "message": "Знак „хеш“",
"description": "Represents the # key in screen reader content as a readable word" "description": "Represents the # key in screen reader content as a readable word"
}, },
"dollarSignCharacterDescriptor": { "dollarSignCharacterDescriptor": {
"message": "Dollar sign", "message": "Знак долар",
"description": "Represents the $ key in screen reader content as a readable word" "description": "Represents the $ key in screen reader content as a readable word"
}, },
"percentSignCharacterDescriptor": { "percentSignCharacterDescriptor": {
"message": "Percent sign", "message": "Знак постотак",
"description": "Represents the % key in screen reader content as a readable word" "description": "Represents the % key in screen reader content as a readable word"
}, },
"caretCharacterDescriptor": { "caretCharacterDescriptor": {
"message": "Caret", "message": "Знак за уметање",
"description": "Represents the ^ key in screen reader content as a readable word" "description": "Represents the ^ key in screen reader content as a readable word"
}, },
"ampersandCharacterDescriptor": { "ampersandCharacterDescriptor": {
"message": "Ampersand", "message": "Знак Ampersand",
"description": "Represents the & key in screen reader content as a readable word" "description": "Represents the & key in screen reader content as a readable word"
}, },
"asteriskCharacterDescriptor": { "asteriskCharacterDescriptor": {
"message": "Asterisk", "message": "Знак звездица",
"description": "Represents the * key in screen reader content as a readable word" "description": "Represents the * key in screen reader content as a readable word"
}, },
"parenLeftCharacterDescriptor": { "parenLeftCharacterDescriptor": {
"message": "Left parenthesis", "message": "Отворена заграда",
"description": "Represents the ( key in screen reader content as a readable word" "description": "Represents the ( key in screen reader content as a readable word"
}, },
"parenRightCharacterDescriptor": { "parenRightCharacterDescriptor": {
"message": "Right parenthesis", "message": "Затворена заграда",
"description": "Represents the ) key in screen reader content as a readable word" "description": "Represents the ) key in screen reader content as a readable word"
}, },
"hyphenCharacterDescriptor": { "hyphenCharacterDescriptor": {
"message": "Underscore", "message": "Доња црта",
"description": "Represents the _ key in screen reader content as a readable word" "description": "Represents the _ key in screen reader content as a readable word"
}, },
"underscoreCharacterDescriptor": { "underscoreCharacterDescriptor": {
"message": "Hyphen", "message": "Цртица",
"description": "Represents the - key in screen reader content as a readable word" "description": "Represents the - key in screen reader content as a readable word"
}, },
"plusCharacterDescriptor": { "plusCharacterDescriptor": {
"message": "Plus", "message": "Плус",
"description": "Represents the + key in screen reader content as a readable word" "description": "Represents the + key in screen reader content as a readable word"
}, },
"equalsCharacterDescriptor": { "equalsCharacterDescriptor": {

View File

@ -1511,7 +1511,7 @@
"message": "Komprometterade eller ej betrodda webbplatser kan utnyttja automatisk ifyllnad vid sidladdning." "message": "Komprometterade eller ej betrodda webbplatser kan utnyttja automatisk ifyllnad vid sidladdning."
}, },
"learnMoreAboutAutofillOnPageLoadLinkText": { "learnMoreAboutAutofillOnPageLoadLinkText": {
"message": "Learn more about risks" "message": "Läs mer om risker"
}, },
"learnMoreAboutAutofill": { "learnMoreAboutAutofill": {
"message": "Läs mer om automatisk ifyllnad" "message": "Läs mer om automatisk ifyllnad"
@ -2411,7 +2411,7 @@
"message": "Lösenordsskyddad" "message": "Lösenordsskyddad"
}, },
"copyLink": { "copyLink": {
"message": "Copy link" "message": "Kopiera länk"
}, },
"copySendLink": { "copySendLink": {
"message": "Kopiera Send-länk", "message": "Kopiera Send-länk",

View File

@ -2878,7 +2878,7 @@
"message": "Генерувати е-пошту" "message": "Генерувати е-пошту"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$", "message": "Значення має бути між $MIN$ та $MAX$",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {

View File

@ -20,16 +20,16 @@
"message": "创建账户" "message": "创建账户"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "Bitwarden 新手吗?"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "使用通行密钥登录"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "使用单点登录"
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "欢迎回来"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "设置强密码" "message": "设置强密码"
@ -120,7 +120,7 @@
"message": "复制密码" "message": "复制密码"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase" "message": "复制密码短语"
}, },
"copyNote": { "copyNote": {
"message": "复制备注" "message": "复制备注"
@ -168,7 +168,7 @@
"message": "复制备注" "message": "复制备注"
}, },
"fill": { "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." "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": { "autoFill": {
@ -427,7 +427,7 @@
"message": "生成密码" "message": "生成密码"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "生成密码短语"
}, },
"regeneratePassword": { "regeneratePassword": {
"message": "重新生成密码" "message": "重新生成密码"
@ -591,7 +591,7 @@
"message": "启动网站" "message": "启动网站"
}, },
"launchWebsiteName": { "launchWebsiteName": {
"message": "Launch website $ITEMNAME$", "message": "前往 $ITEMNAME$ 的网站",
"placeholders": { "placeholders": {
"itemname": { "itemname": {
"content": "$1", "content": "$1",
@ -846,7 +846,7 @@
"message": "登录" "message": "登录"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "登录到 Bitwarden"
}, },
"restartRegistration": { "restartRegistration": {
"message": "重新开始注册" "message": "重新开始注册"
@ -1424,7 +1424,7 @@
"message": "服务器 URL" "message": "服务器 URL"
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "自托管服务器 URL",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@ -1795,13 +1795,13 @@
"message": "密码历史记录" "message": "密码历史记录"
}, },
"generatorHistory": { "generatorHistory": {
"message": "Generator history" "message": "生成器历史记录"
}, },
"clearGeneratorHistoryTitle": { "clearGeneratorHistoryTitle": {
"message": "Clear generator history" "message": "清除生成器历史记录"
}, },
"cleargGeneratorHistoryDescription": { "cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" "message": "若继续,所有条目将从生成器历史记录中永久删除。确定要继续吗?"
}, },
"back": { "back": {
"message": "后退" "message": "后退"
@ -1920,10 +1920,10 @@
"message": "清除历史记录" "message": "清除历史记录"
}, },
"nothingToShow": { "nothingToShow": {
"message": "Nothing to show" "message": "没有可显示的内容"
}, },
"nothingGeneratedRecently": { "nothingGeneratedRecently": {
"message": "You haven't generated anything recently" "message": "您最近没有生成任何内容"
}, },
"remove": { "remove": {
"message": "移除" "message": "移除"
@ -2512,7 +2512,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendPasswordDescV3": { "sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.", "message": "添加一个用于收件人访问此 Send 的可选密码。",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
}, },
"sendNotesDesc": { "sendNotesDesc": {
@ -2875,10 +2875,10 @@
"message": "生成用户名" "message": "生成用户名"
}, },
"generateEmail": { "generateEmail": {
"message": "Generate email" "message": "生成邮件地址"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$", "message": "值必须在 $MIN$ 和 $MAX$ 之间",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@ -2932,11 +2932,11 @@
"message": "使用外部转发服务生成一个电子邮件别名。" "message": "使用外部转发服务生成一个电子邮件别名。"
}, },
"forwarderDomainName": { "forwarderDomainName": {
"message": "Email domain", "message": "邮件域名",
"description": "Labels the domain name email forwarder service option" "description": "Labels the domain name email forwarder service option"
}, },
"forwarderDomainNameHint": { "forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service", "message": "选择一个所选服务支持的域名",
"description": "Guidance provided for email forwarding services that support multiple email domains." "description": "Guidance provided for email forwarding services that support multiple email domains."
}, },
"forwarderError": { "forwarderError": {
@ -4636,156 +4636,156 @@
"message": "正在验证" "message": "正在验证"
}, },
"fillGeneratedPassword": { "fillGeneratedPassword": {
"message": "Fill generated password", "message": "填充已生成的密码",
"description": "Heading for the password generator within the inline menu" "description": "Heading for the password generator within the inline menu"
}, },
"passwordRegenerated": { "passwordRegenerated": {
"message": "Password regenerated", "message": "密码已重新生成",
"description": "Notification message for when a password has been regenerated" "description": "Notification message for when a password has been regenerated"
}, },
"saveLoginToBitwarden": { "saveLoginToBitwarden": {
"message": "Save login to Bitwarden?", "message": "将登录保存到 Bitwarden 吗?",
"description": "Confirmation message for saving a login to Bitwarden" "description": "Confirmation message for saving a login to Bitwarden"
}, },
"spaceCharacterDescriptor": { "spaceCharacterDescriptor": {
"message": "Space", "message": "空格",
"description": "Represents the space key in screen reader content as a readable word" "description": "Represents the space key in screen reader content as a readable word"
}, },
"tildeCharacterDescriptor": { "tildeCharacterDescriptor": {
"message": "Tilde", "message": "波浪号",
"description": "Represents the ~ key in screen reader content as a readable word" "description": "Represents the ~ key in screen reader content as a readable word"
}, },
"backtickCharacterDescriptor": { "backtickCharacterDescriptor": {
"message": "Backtick", "message": "反引号",
"description": "Represents the ` key in screen reader content as a readable word" "description": "Represents the ` key in screen reader content as a readable word"
}, },
"exclamationCharacterDescriptor": { "exclamationCharacterDescriptor": {
"message": "Exclamation mark", "message": "感叹号",
"description": "Represents the ! key in screen reader content as a readable word" "description": "Represents the ! key in screen reader content as a readable word"
}, },
"atSignCharacterDescriptor": { "atSignCharacterDescriptor": {
"message": "At sign", "message": "艾特号",
"description": "Represents the @ key in screen reader content as a readable word" "description": "Represents the @ key in screen reader content as a readable word"
}, },
"hashSignCharacterDescriptor": { "hashSignCharacterDescriptor": {
"message": "Hash sign", "message": "井号",
"description": "Represents the # key in screen reader content as a readable word" "description": "Represents the # key in screen reader content as a readable word"
}, },
"dollarSignCharacterDescriptor": { "dollarSignCharacterDescriptor": {
"message": "Dollar sign", "message": "美元符号",
"description": "Represents the $ key in screen reader content as a readable word" "description": "Represents the $ key in screen reader content as a readable word"
}, },
"percentSignCharacterDescriptor": { "percentSignCharacterDescriptor": {
"message": "Percent sign", "message": "百分号",
"description": "Represents the % key in screen reader content as a readable word" "description": "Represents the % key in screen reader content as a readable word"
}, },
"caretCharacterDescriptor": { "caretCharacterDescriptor": {
"message": "Caret", "message": "脱字符",
"description": "Represents the ^ key in screen reader content as a readable word" "description": "Represents the ^ key in screen reader content as a readable word"
}, },
"ampersandCharacterDescriptor": { "ampersandCharacterDescriptor": {
"message": "Ampersand", "message": "与和符",
"description": "Represents the & key in screen reader content as a readable word" "description": "Represents the & key in screen reader content as a readable word"
}, },
"asteriskCharacterDescriptor": { "asteriskCharacterDescriptor": {
"message": "Asterisk", "message": "星号",
"description": "Represents the * key in screen reader content as a readable word" "description": "Represents the * key in screen reader content as a readable word"
}, },
"parenLeftCharacterDescriptor": { "parenLeftCharacterDescriptor": {
"message": "Left parenthesis", "message": "左括号",
"description": "Represents the ( key in screen reader content as a readable word" "description": "Represents the ( key in screen reader content as a readable word"
}, },
"parenRightCharacterDescriptor": { "parenRightCharacterDescriptor": {
"message": "Right parenthesis", "message": "右括号",
"description": "Represents the ) key in screen reader content as a readable word" "description": "Represents the ) key in screen reader content as a readable word"
}, },
"hyphenCharacterDescriptor": { "hyphenCharacterDescriptor": {
"message": "Underscore", "message": "下划线",
"description": "Represents the _ key in screen reader content as a readable word" "description": "Represents the _ key in screen reader content as a readable word"
}, },
"underscoreCharacterDescriptor": { "underscoreCharacterDescriptor": {
"message": "Hyphen", "message": "连字符",
"description": "Represents the - key in screen reader content as a readable word" "description": "Represents the - key in screen reader content as a readable word"
}, },
"plusCharacterDescriptor": { "plusCharacterDescriptor": {
"message": "Plus", "message": "加号",
"description": "Represents the + key in screen reader content as a readable word" "description": "Represents the + key in screen reader content as a readable word"
}, },
"equalsCharacterDescriptor": { "equalsCharacterDescriptor": {
"message": "Equals", "message": "等号",
"description": "Represents the = key in screen reader content as a readable word" "description": "Represents the = key in screen reader content as a readable word"
}, },
"braceLeftCharacterDescriptor": { "braceLeftCharacterDescriptor": {
"message": "Left brace", "message": "左大括号",
"description": "Represents the { key in screen reader content as a readable word" "description": "Represents the { key in screen reader content as a readable word"
}, },
"braceRightCharacterDescriptor": { "braceRightCharacterDescriptor": {
"message": "Right brace", "message": "右大括号",
"description": "Represents the } key in screen reader content as a readable word" "description": "Represents the } key in screen reader content as a readable word"
}, },
"bracketLeftCharacterDescriptor": { "bracketLeftCharacterDescriptor": {
"message": "Left bracket", "message": "左中括号",
"description": "Represents the [ key in screen reader content as a readable word" "description": "Represents the [ key in screen reader content as a readable word"
}, },
"bracketRightCharacterDescriptor": { "bracketRightCharacterDescriptor": {
"message": "Right bracket", "message": "右中括号",
"description": "Represents the ] key in screen reader content as a readable word" "description": "Represents the ] key in screen reader content as a readable word"
}, },
"pipeCharacterDescriptor": { "pipeCharacterDescriptor": {
"message": "Pipe", "message": "竖线",
"description": "Represents the | key in screen reader content as a readable word" "description": "Represents the | key in screen reader content as a readable word"
}, },
"backSlashCharacterDescriptor": { "backSlashCharacterDescriptor": {
"message": "Back slash", "message": "反斜杠",
"description": "Represents the back slash key in screen reader content as a readable word" "description": "Represents the back slash key in screen reader content as a readable word"
}, },
"colonCharacterDescriptor": { "colonCharacterDescriptor": {
"message": "Colon", "message": "冒号",
"description": "Represents the : key in screen reader content as a readable word" "description": "Represents the : key in screen reader content as a readable word"
}, },
"semicolonCharacterDescriptor": { "semicolonCharacterDescriptor": {
"message": "Semicolon", "message": "分号",
"description": "Represents the ; key in screen reader content as a readable word" "description": "Represents the ; key in screen reader content as a readable word"
}, },
"doubleQuoteCharacterDescriptor": { "doubleQuoteCharacterDescriptor": {
"message": "Double quote", "message": "双引号",
"description": "Represents the double quote key in screen reader content as a readable word" "description": "Represents the double quote key in screen reader content as a readable word"
}, },
"singleQuoteCharacterDescriptor": { "singleQuoteCharacterDescriptor": {
"message": "Single quote", "message": "单引号",
"description": "Represents the ' key in screen reader content as a readable word" "description": "Represents the ' key in screen reader content as a readable word"
}, },
"lessThanCharacterDescriptor": { "lessThanCharacterDescriptor": {
"message": "Less than", "message": "小于号",
"description": "Represents the < key in screen reader content as a readable word" "description": "Represents the < key in screen reader content as a readable word"
}, },
"greaterThanCharacterDescriptor": { "greaterThanCharacterDescriptor": {
"message": "Greater than", "message": "大于号",
"description": "Represents the > key in screen reader content as a readable word" "description": "Represents the > key in screen reader content as a readable word"
}, },
"commaCharacterDescriptor": { "commaCharacterDescriptor": {
"message": "Comma", "message": "逗号",
"description": "Represents the , key in screen reader content as a readable word" "description": "Represents the , key in screen reader content as a readable word"
}, },
"periodCharacterDescriptor": { "periodCharacterDescriptor": {
"message": "Period", "message": "句号",
"description": "Represents the . key in screen reader content as a readable word" "description": "Represents the . key in screen reader content as a readable word"
}, },
"questionCharacterDescriptor": { "questionCharacterDescriptor": {
"message": "Question mark", "message": "问号",
"description": "Represents the ? key in screen reader content as a readable word" "description": "Represents the ? key in screen reader content as a readable word"
}, },
"forwardSlashCharacterDescriptor": { "forwardSlashCharacterDescriptor": {
"message": "Forward slash", "message": "正斜杠",
"description": "Represents the / key in screen reader content as a readable word" "description": "Represents the / key in screen reader content as a readable word"
}, },
"lowercaseAriaLabel": { "lowercaseAriaLabel": {
"message": "Lowercase" "message": "小写"
}, },
"uppercaseAriaLabel": { "uppercaseAriaLabel": {
"message": "Uppercase" "message": "大写"
}, },
"generatedPassword": { "generatedPassword": {
"message": "Generated password" "message": "生成密码"
} }
} }

View File

@ -9,7 +9,7 @@
<ng-container slot="end"> <ng-container slot="end">
<app-pop-out></app-pop-out> <app-pop-out></app-pop-out>
<app-current-account *ngIf="showAcctSwitcher"></app-current-account> <app-current-account *ngIf="showAcctSwitcher && hasLoggedInAccount"></app-current-account>
</ng-container> </ng-container>
</popup-header> </popup-header>

View File

@ -15,6 +15,7 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { CurrentAccountComponent } from "../account-switching/current-account.component"; import { CurrentAccountComponent } from "../account-switching/current-account.component";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon"; import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon";
@ -50,6 +51,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected pageIcon: Icon; protected pageIcon: Icon;
protected showReadonlyHostname: boolean; protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl"; protected maxWidth: "md" | "3xl";
protected hasLoggedInAccount: boolean = false;
protected theme: string; protected theme: string;
protected logo = ExtensionBitwardenLogo; protected logo = ExtensionBitwardenLogo;
@ -59,6 +61,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private i18nService: I18nService, private i18nService: I18nService,
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService, private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private accountSwitcherService: AccountSwitcherService,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
@ -68,6 +71,12 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
// Listen for page changes and update the page data appropriately // Listen for page changes and update the page data appropriately
this.listenForPageDataChanges(); this.listenForPageDataChanges();
this.listenForServiceDataChanges(); this.listenForServiceDataChanges();
this.accountSwitcherService.availableAccounts$
.pipe(takeUntil(this.destroy$))
.subscribe((accounts) => {
this.hasLoggedInAccount = accounts.some((account) => account.id !== "addAccount");
});
} }
private listenForPageDataChanges() { private listenForPageDataChanges() {

View File

@ -27,6 +27,7 @@ import { ButtonModule, I18nMockService } from "@bitwarden/components";
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service"; import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
import { import {
@ -45,6 +46,7 @@ const decorators = (options: {
applicationVersion?: string; applicationVersion?: string;
clientType?: ClientType; clientType?: ClientType;
hostName?: string; hostName?: string;
accounts?: any[];
}) => { }) => {
return [ return [
componentWrapperDecorator( componentWrapperDecorator(
@ -83,6 +85,13 @@ const decorators = (options: {
}), }),
}, },
}, },
{
provide: AccountSwitcherService,
useValue: {
availableAccounts$: of(options.accounts || []),
SPECIAL_ADD_ACCOUNT_ID: "addAccount",
} as Partial<AccountSwitcherService>,
},
{ {
provide: AuthService, provide: AuthService,
useValue: { useValue: {
@ -300,3 +309,64 @@ export const DynamicContentExample: Story = {
], ],
}), }),
}; };
export const HasLoggedInAccountExample: Story = {
render: (args) => ({
props: args,
template: "<router-outlet></router-outlet>",
}),
decorators: decorators({
components: [DefaultPrimaryOutletExampleComponent],
routes: [
{
path: "**",
redirectTo: "has-logged-in-account",
pathMatch: "full",
},
{
path: "",
component: ExtensionAnonLayoutWrapperComponent,
children: [
{
path: "has-logged-in-account",
data: {
hasLoggedInAccount: true,
showAcctSwitcher: true,
},
children: [
{
path: "",
component: DefaultPrimaryOutletExampleComponent,
},
{
path: "",
component: DefaultSecondaryOutletExampleComponent,
outlet: "secondary",
},
{
path: "",
component: DefaultEnvSelectorOutletExampleComponent,
outlet: "environment-selector",
},
],
},
],
},
],
accounts: [
{
name: "Test User",
email: "testuser@bitwarden.com",
id: "123e4567-e89b-12d3-a456-426614174000",
server: "bitwarden.com",
status: 2,
isActive: false,
},
{
name: "addAccount",
id: "addAccount",
isActive: false,
},
],
}),
};

View File

@ -1,10 +1,12 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs"; import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components"; import { ToastService } from "@bitwarden/components";
@ -38,9 +40,13 @@ export class HomeComponent implements OnInit, OnDestroy {
private accountSwitcherService: AccountSwitcherService, private accountSwitcherService: AccountSwitcherService,
private registerRouteService: RegisterRouteService, private registerRouteService: RegisterRouteService,
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService,
private route: ActivatedRoute,
) {} ) {}
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.listenForUnauthUiRefreshFlagChanges();
const email = await firstValueFrom(this.loginEmailService.loginEmail$); const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail(); const rememberEmail = this.loginEmailService.getRememberEmail();
@ -70,6 +76,29 @@ export class HomeComponent implements OnInit, OnDestroy {
this.destroyed$.complete(); this.destroyed$.complete();
} }
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/login"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroyed$),
)
.subscribe();
}
get availableAccounts$() { get availableAccounts$() {
return this.accountSwitcherService.availableAccounts$; return this.accountSwitcherService.availableAccounts$;
} }

View File

@ -216,7 +216,6 @@ export type OverlayBackgroundExtensionMessageHandlers = {
getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number; getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number;
updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void;
triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void; triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void;
shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }: BackgroundSenderParam) => void;
destroyAutofillInlineMenuListeners: ({ destroyAutofillInlineMenuListeners: ({
message, message,
sender, sender,

View File

@ -32,6 +32,7 @@ import {
} from "@bitwarden/common/spec"; } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -106,6 +107,7 @@ describe("OverlayBackground", () => {
let selectedThemeMock$: BehaviorSubject<ThemeType>; let selectedThemeMock$: BehaviorSubject<ThemeType>;
let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
let themeStateService: MockProxy<ThemeStateService>; let themeStateService: MockProxy<ThemeStateService>;
let totpService: MockProxy<TotpService>;
let overlayBackground: OverlayBackground; let overlayBackground: OverlayBackground;
let portKeyForTabSpy: Record<number, string>; let portKeyForTabSpy: Record<number, string>;
let pageDetailsForTabSpy: PageDetailsForTab; let pageDetailsForTabSpy: PageDetailsForTab;
@ -184,6 +186,7 @@ describe("OverlayBackground", () => {
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
themeStateService = mock<ThemeStateService>(); themeStateService = mock<ThemeStateService>();
themeStateService.selectedTheme$ = selectedThemeMock$; themeStateService.selectedTheme$ = selectedThemeMock$;
totpService = mock<TotpService>();
overlayBackground = new OverlayBackground( overlayBackground = new OverlayBackground(
logService, logService,
cipherService, cipherService,
@ -198,6 +201,7 @@ describe("OverlayBackground", () => {
fido2ActiveRequestManager, fido2ActiveRequestManager,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
themeStateService, themeStateService,
totpService,
generatedPasswordCallbackMock, generatedPasswordCallbackMock,
addPasswordCallbackMock, addPasswordCallbackMock,
); );
@ -629,9 +633,7 @@ describe("OverlayBackground", () => {
it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => { it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => {
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
jest overlayBackground["inlineMenuListPort"] = null;
.spyOn(overlayBackground as any, "checkIsInlineMenuListVisible")
.mockReturnValue(false);
tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { tabsSendMessageSpy.mockImplementation((_tab, message, _options) => {
if (message.command === "checkFocusedFieldHasValue") { if (message.command === "checkFocusedFieldHasValue") {
return Promise.resolve(true); return Promise.resolve(true);
@ -2267,7 +2269,7 @@ describe("OverlayBackground", () => {
}); });
it("closes the list if the user has the inline menu set to show on button click and the list is open", async () => { it("closes the list if the user has the inline menu set to show on button click and the list is open", async () => {
overlayBackground["isInlineMenuListVisible"] = true; overlayBackground["inlineMenuListPort"] = listPortSpy;
inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick);
sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender);

View File

@ -33,6 +33,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
@ -168,8 +169,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender), getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender),
updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender), updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender),
triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender), triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender),
shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }) =>
this.shouldRepositionSubFrameInlineMenuOnScroll(sender),
destroyAutofillInlineMenuListeners: ({ message, sender }) => destroyAutofillInlineMenuListeners: ({ message, sender }) =>
this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId),
collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender),
@ -219,6 +218,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private fido2ActiveRequestManager: Fido2ActiveRequestManager, private fido2ActiveRequestManager: Fido2ActiveRequestManager,
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
private themeStateService: ThemeStateService, private themeStateService: ThemeStateService,
private totpService: TotpService,
private generatePasswordCallback: () => Promise<string>, private generatePasswordCallback: () => Promise<string>,
private addPasswordCallback: (password: string) => Promise<void>, private addPasswordCallback: (password: string) => Promise<void>,
) { ) {
@ -1010,7 +1010,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
); );
if ( if (
!this.checkIsInlineMenuListVisible() && !this.inlineMenuListPort &&
(await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick (await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick
) { ) {
return; return;
@ -1060,7 +1060,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
} }
const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId);
if (usePasskey && cipher.login?.hasFido2Credentials) { if (usePasskey && cipher.login?.hasFido2Credentials) {
await this.authenticatePasskeyCredential( await this.authenticatePasskeyCredential(
sender, sender,
@ -1068,6 +1067,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
); );
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
if (cipher.login?.totp) {
this.platformUtilsService.copyToClipboard(
await this.totpService.getCode(cipher.login.totp),
);
}
return; return;
} }
@ -1819,7 +1823,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
return; return;
} }
if (this.isInlineMenuListVisible) { if (this.inlineMenuListPort) {
this.closeInlineMenu(sender, { this.closeInlineMenu(sender, {
forceCloseInlineMenu: true, forceCloseInlineMenu: true,
overlayElement: AutofillOverlayElement.List, overlayElement: AutofillOverlayElement.List,
@ -2600,20 +2604,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
this.repositionInlineMenu$.next(sender); this.repositionInlineMenu$.next(sender);
} }
/**
* Triggers on scroll of a frame within the tab. Will reposition the inline menu
* if the focused field is within a sub-frame and the inline menu is visible.
*
* @param sender - The sender of the message
*/
private shouldRepositionSubFrameInlineMenuOnScroll(sender: chrome.runtime.MessageSender) {
if (!this.isInlineMenuButtonVisible || sender.tab.id !== this.focusedFieldData?.tabId) {
return false;
}
return this.focusedFieldData.frameId > 0;
}
/** /**
* Handles determining if the inline menu should be repositioned or closed, and initiates * Handles determining if the inline menu should be repositioned or closed, and initiates
* the process of calculating the new position of the inline menu. * the process of calculating the new position of the inline menu.

View File

@ -219,8 +219,12 @@ export class AutofillComponent implements OnInit {
: AutofillOverlayVisibility.Off; : AutofillOverlayVisibility.Off;
await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue); await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue);
// No need to initiate browser permission request if a feature is being turned off
if (newInlineMenuVisibilityValue !== AutofillOverlayVisibility.Off) {
await this.requestPrivacyPermission(); await this.requestPrivacyPermission();
} }
}
async updateAutofillOnPageLoad() { async updateAutofillOnPageLoad() {
await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutofillOnPageLoad); await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutofillOnPageLoad);

View File

@ -104,6 +104,7 @@ export class CreditCardAutoFillConstants {
]; ];
static readonly CardHolderFieldNames: string[] = [ static readonly CardHolderFieldNames: string[] = [
"accountholdername",
"cc-name", "cc-name",
"card-name", "card-name",
"cardholder-name", "cardholder-name",
@ -113,6 +114,7 @@ export class CreditCardAutoFillConstants {
]; ];
static readonly CardHolderFieldNameValues: string[] = [ static readonly CardHolderFieldNameValues: string[] = [
"accountholdername",
"cc-name", "cc-name",
"card-name", "card-name",
"cardholder-name", "cardholder-name",

View File

@ -1703,6 +1703,10 @@ describe("AutofillOverlayContentService", () => {
const repositionEvents = [EVENTS.SCROLL, EVENTS.RESIZE]; const repositionEvents = [EVENTS.SCROLL, EVENTS.RESIZE];
repositionEvents.forEach((repositionEvent) => { repositionEvents.forEach((repositionEvent) => {
it(`sends a message trigger overlay reposition message to the background when a ${repositionEvent} event occurs`, async () => { it(`sends a message trigger overlay reposition message to the background when a ${repositionEvent} event occurs`, async () => {
Object.defineProperty(globalThis, "scrollY", {
value: 10,
writable: true,
});
sendExtensionMessageSpy.mockResolvedValueOnce(true); sendExtensionMessageSpy.mockResolvedValueOnce(true);
globalThis.dispatchEvent(new Event(repositionEvent)); globalThis.dispatchEvent(new Event(repositionEvent));
await flushPromises(); await flushPromises();

View File

@ -1568,41 +1568,46 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
* the overlay elements on scroll or resize. * the overlay elements on scroll or resize.
*/ */
private setOverlayRepositionEventListeners() { private setOverlayRepositionEventListeners() {
let currentScrollY = globalThis.scrollY;
let currentScrollX = globalThis.scrollX;
let mostRecentTargetScrollY = 0;
const repositionHandler = this.useEventHandlersMemo( const repositionHandler = this.useEventHandlersMemo(
throttle(this.handleOverlayRepositionEvent, 250), throttle(this.handleOverlayRepositionEvent, 250),
AUTOFILL_OVERLAY_HANDLE_REPOSITION, AUTOFILL_OVERLAY_HANDLE_REPOSITION,
); );
const eventTargetContainsFocusedField = (eventTarget: Element | Document) => { const eventTargetContainsFocusedField = (eventTarget: Element) => {
if (!eventTarget || !this.mostRecentlyFocusedField) {
return false;
}
const activeElement = (eventTarget as Document).activeElement;
if (activeElement) {
return (
activeElement === this.mostRecentlyFocusedField ||
activeElement.contains(this.mostRecentlyFocusedField) ||
this.inlineMenuContentService?.isElementInlineMenu(activeElement as HTMLElement)
);
}
if (typeof eventTarget.contains !== "function") { if (typeof eventTarget.contains !== "function") {
return false; return false;
} }
return (
const targetScrollY = eventTarget.scrollTop;
if (targetScrollY === mostRecentTargetScrollY) {
return false;
}
if (
eventTarget === this.mostRecentlyFocusedField || eventTarget === this.mostRecentlyFocusedField ||
eventTarget.contains(this.mostRecentlyFocusedField) eventTarget.contains(this.mostRecentlyFocusedField)
); ) {
mostRecentTargetScrollY = targetScrollY;
return true;
}
return false;
}; };
const scrollHandler = this.useEventHandlersMemo( const scrollHandler = this.useEventHandlersMemo(
throttle(async (event) => { throttle(async (event) => {
if ( if (
eventTargetContainsFocusedField(event.target) || currentScrollY !== globalThis.scrollY ||
(await this.shouldRepositionSubFrameInlineMenuOnScroll()) currentScrollX !== globalThis.scrollX ||
eventTargetContainsFocusedField(event.target)
) { ) {
repositionHandler(event); repositionHandler(event);
} }
currentScrollY = globalThis.scrollY;
currentScrollX = globalThis.scrollX;
}, 50), }, 50),
AUTOFILL_OVERLAY_HANDLE_SCROLL, AUTOFILL_OVERLAY_HANDLE_SCROLL,
); );

View File

@ -980,7 +980,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
const queueLength = this.mutationsQueue.length; const queueLength = this.mutationsQueue.length;
if (!this.domQueryService.pageContainsShadowDomElements()) { if (!this.domQueryService.pageContainsShadowDomElements()) {
this.domQueryService.checkPageContainsShadowDom(); this.checkPageContainsShadowDom();
} }
for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) { for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) {
@ -999,6 +999,29 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
this.mutationsQueue = []; this.mutationsQueue = [];
}; };
/**
* Handles checking if the current page contains a ShadowDOM element and
* flags that a re-collection of page details is required if it does.
*/
private checkPageContainsShadowDom() {
this.domQueryService.checkPageContainsShadowDom();
if (this.domQueryService.pageContainsShadowDomElements()) {
this.flagPageDetailsUpdateIsRequired();
}
}
/**
* Triggers several flags that indicate that a collection of page details should
* occur again on a subsequent call after a mutation has been observed in the DOM.
*/
private flagPageDetailsUpdateIsRequired() {
this.domRecentlyMutated = true;
if (this.autofillOverlayContentService) {
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
}
this.noFieldsFound = false;
}
/** /**
* Processes all mutation records encountered by the mutation observer. * Processes all mutation records encountered by the mutation observer.
* *
@ -1023,11 +1046,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
(this.isAutofillElementNodeMutated(mutation.removedNodes, true) || (this.isAutofillElementNodeMutated(mutation.removedNodes, true) ||
this.isAutofillElementNodeMutated(mutation.addedNodes)) this.isAutofillElementNodeMutated(mutation.addedNodes))
) { ) {
this.domRecentlyMutated = true; this.flagPageDetailsUpdateIsRequired();
if (this.autofillOverlayContentService) {
this.autofillOverlayContentService.pageDetailsUpdateRequired = true;
}
this.noFieldsFound = false;
return; return;
} }

View File

@ -257,12 +257,9 @@ import { BrowserPlatformUtilsService } from "../platform/services/platform-utils
import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service"; import { PopupViewCacheBackgroundService } from "../platform/services/popup-view-cache-background.service";
import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory"; import { BrowserSdkClientFactory } from "../platform/services/sdk/browser-sdk-client-factory";
import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service"; import { BackgroundTaskSchedulerService } from "../platform/services/task-scheduler/background-task-scheduler.service";
import { ForegroundTaskSchedulerService } from "../platform/services/task-scheduler/foreground-task-scheduler.service";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service";
import { OffscreenStorageService } from "../platform/storage/offscreen-storage.service"; import { OffscreenStorageService } from "../platform/storage/offscreen-storage.service";
import { ForegroundSyncService } from "../platform/sync/foreground-sync.service";
import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { SyncServiceListener } from "../platform/sync/sync-service.listener";
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
@ -401,7 +398,7 @@ export default class MainBackground {
private popupViewCacheBackgroundService: PopupViewCacheBackgroundService; private popupViewCacheBackgroundService: PopupViewCacheBackgroundService;
constructor(public popupOnlyContext: boolean = false) { constructor() {
// Services // Services
const lockedCallback = async (userId?: string) => { const lockedCallback = async (userId?: string) => {
if (this.notificationsService != null) { if (this.notificationsService != null) {
@ -460,6 +457,18 @@ export default class MainBackground {
this.offscreenDocumentService, this.offscreenDocumentService,
); );
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
if (BrowserApi.isManifestVersion(3)) {
// manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3
this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session
this.memoryStorageService = this.memoryStorageForStateProviders;
} else {
this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageService = this.memoryStorageForStateProviders;
}
if (BrowserApi.isManifestVersion(3)) {
// Creates a session key for mv3 storage of large memory items // Creates a session key for mv3 storage of large memory items
const sessionKey = new Lazy(async () => { const sessionKey = new Lazy(async () => {
// Key already in session storage // Key already in session storage
@ -482,42 +491,20 @@ export default class MainBackground {
return derivedKey; return derivedKey;
}); });
const mv3MemoryStorageCreator = () => { this.largeObjectMemoryStorageForStateProviders = new LocalBackedSessionStorageService(
if (this.popupOnlyContext) { sessionKey,
return new ForegroundMemoryStorageService(); this.storageService,
}
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory // 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` // 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. // so that MAC failures are not logged.
return new LocalBackedSessionStorageService(
sessionKey,
this.storageService,
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
this.platformUtilsService, this.platformUtilsService,
this.logService, this.logService,
); );
};
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
if (BrowserApi.isManifestVersion(3)) {
// manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3
this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session
this.memoryStorageService = this.memoryStorageForStateProviders;
} else { } else {
if (popupOnlyContext) { // mv2 stores to the same location
this.memoryStorageForStateProviders = new ForegroundMemoryStorageService(); this.largeObjectMemoryStorageForStateProviders = this.memoryStorageForStateProviders;
this.memoryStorageService = new ForegroundMemoryStorageService();
} else {
this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageService = this.memoryStorageForStateProviders;
} }
}
this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage
: this.memoryStorageForStateProviders; // mv2 stores to the same location
const localStorageStorageService = BrowserApi.isManifestVersion(3) const localStorageStorageService = BrowserApi.isManifestVersion(3)
? new OffscreenStorageService(this.offscreenDocumentService) ? new OffscreenStorageService(this.offscreenDocumentService)
@ -575,9 +562,10 @@ export default class MainBackground {
this.derivedStateProvider, this.derivedStateProvider,
); );
this.taskSchedulerService = this.popupOnlyContext this.taskSchedulerService = new BackgroundTaskSchedulerService(
? new ForegroundTaskSchedulerService(this.logService, this.stateProvider) this.logService,
: new BackgroundTaskSchedulerService(this.logService, this.stateProvider); this.stateProvider,
);
this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () => this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () =>
this.fullSync(), this.fullSync(),
); );
@ -632,6 +620,7 @@ export default class MainBackground {
this.stateService, this.stateService,
this.keyGenerationService, this.keyGenerationService,
this.encryptService, this.encryptService,
this.logService,
); );
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
@ -872,7 +861,6 @@ export default class MainBackground {
this.vaultSettingsService = new VaultSettingsService(this.stateProvider); this.vaultSettingsService = new VaultSettingsService(this.stateProvider);
if (!this.popupOnlyContext) {
this.vaultTimeoutService = new VaultTimeoutService( this.vaultTimeoutService = new VaultTimeoutService(
this.accountService, this.accountService,
this.masterPasswordService, this.masterPasswordService,
@ -891,7 +879,6 @@ export default class MainBackground {
lockedCallback, lockedCallback,
logoutCallback, logoutCallback,
); );
}
this.containerService = new ContainerService(this.keyService, this.encryptService); this.containerService = new ContainerService(this.keyService, this.encryptService);
this.sendStateProvider = new SendStateProvider(this.stateProvider); this.sendStateProvider = new SendStateProvider(this.stateProvider);
@ -912,24 +899,6 @@ export default class MainBackground {
this.providerService = new ProviderService(this.stateProvider); this.providerService = new ProviderService(this.stateProvider);
if (this.popupOnlyContext) {
this.syncService = new ForegroundSyncService(
this.stateService,
this.folderService,
this.folderApiService,
this.messagingService,
this.logService,
this.cipherService,
this.collectionService,
this.apiService,
this.accountService,
this.authService,
this.sendService,
this.sendApiService,
messageListener,
this.stateProvider,
);
} else {
this.syncService = new DefaultSyncService( this.syncService = new DefaultSyncService(
this.masterPasswordService, this.masterPasswordService,
this.accountService, this.accountService,
@ -964,7 +933,7 @@ export default class MainBackground {
this.messagingService, this.messagingService,
this.logService, this.logService,
); );
}
this.eventUploadService = new EventUploadService( this.eventUploadService = new EventUploadService(
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
@ -1111,7 +1080,7 @@ export default class MainBackground {
this.isSafari = this.platformUtilsService.isSafari(); this.isSafari = this.platformUtilsService.isSafari();
// Background // Background
if (!this.popupOnlyContext) {
this.fido2Background = new Fido2Background( this.fido2Background = new Fido2Background(
this.logService, this.logService,
this.fido2ActiveRequestManager, this.fido2ActiveRequestManager,
@ -1202,7 +1171,14 @@ export default class MainBackground {
const contextMenuClickedHandler = new ContextMenuClickedHandler( const contextMenuClickedHandler = new ContextMenuClickedHandler(
(options) => this.platformUtilsService.copyToClipboard(options.text), (options) => this.platformUtilsService.copyToClipboard(options.text),
async () => this.generatePasswordToClipboard(), async (_tab) => {
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.passwordGenerationService.addHistory(password);
},
async (tab, cipher) => { async (tab, cipher) => {
this.loginToAutoFill = cipher; this.loginToAutoFill = cipher;
if (tab == null) { if (tab == null) {
@ -1226,7 +1202,6 @@ export default class MainBackground {
); );
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler); this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
}
this.idleBackground = new IdleBackground( this.idleBackground = new IdleBackground(
this.vaultTimeoutService, this.vaultTimeoutService,
@ -1245,7 +1220,6 @@ export default class MainBackground {
this.stateProvider, this.stateProvider,
); );
if (!this.popupOnlyContext) {
this.mainContextMenuHandler = new MainContextMenuHandler( this.mainContextMenuHandler = new MainContextMenuHandler(
this.stateService, this.stateService,
this.autofillSettingsService, this.autofillSettingsService,
@ -1268,7 +1242,6 @@ export default class MainBackground {
chrome.webRequest, chrome.webRequest,
); );
} }
}
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.keyService); this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.keyService);
@ -1282,7 +1255,7 @@ export default class MainBackground {
this.containerService.attachToGlobal(self); this.containerService.attachToGlobal(self);
// Only the "true" background should run migrations // Only the "true" background should run migrations
await this.stateService.init({ runMigrations: !this.popupOnlyContext }); await this.stateService.init({ runMigrations: true });
// This is here instead of in in the InitService b/c we don't plan for // This is here instead of in in the InitService b/c we don't plan for
// side effects to run in the Browser InitService. // side effects to run in the Browser InitService.
@ -1304,10 +1277,6 @@ export default class MainBackground {
this.popupViewCacheBackgroundService.startObservingTabChanges(); this.popupViewCacheBackgroundService.startObservingTabChanges();
if (this.popupOnlyContext) {
return;
}
await this.vaultTimeoutService.init(true); await this.vaultTimeoutService.init(true);
this.fido2Background.init(); this.fido2Background.init();
await this.runtimeBackground.init(); await this.runtimeBackground.init();
@ -1636,7 +1605,6 @@ export default class MainBackground {
*/ */
async initOverlayAndTabsBackground() { async initOverlayAndTabsBackground() {
if ( if (
this.popupOnlyContext ||
this.overlayBackground || this.overlayBackground ||
this.tabsBackground || this.tabsBackground ||
(await firstValueFrom(this.authService.activeAccountStatus$)) === (await firstValueFrom(this.authService.activeAccountStatus$)) ===
@ -1677,6 +1645,7 @@ export default class MainBackground {
this.fido2ActiveRequestManager, this.fido2ActiveRequestManager,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
this.themeStateService, this.themeStateService,
this.totpService,
() => this.generatePassword(), () => this.generatePassword(),
(password) => this.addPasswordToHistory(password), (password) => this.addPasswordToHistory(password),
); );

View File

@ -274,7 +274,11 @@ export class NativeMessagingBackground {
let message = rawMessage as ReceiveMessage; let message = rawMessage as ReceiveMessage;
if (!this.platformUtilsService.isSafari()) { if (!this.platformUtilsService.isSafari()) {
message = JSON.parse( message = JSON.parse(
await this.encryptService.decryptToUtf8(rawMessage as EncString, this.sharedSecret), await this.encryptService.decryptToUtf8(
rawMessage as EncString,
this.sharedSecret,
"ipc-desktop-ipc-channel-key",
),
); );
} }

View File

@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.10.1", "version": "2024.11.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0", "minimum_chrome_version": "102.0",
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.10.1", "version": "2024.11.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@ -2,29 +2,6 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import MainBackground from "../background/main.background"; import MainBackground from "../background/main.background";
import { BrowserApi } from "./browser/browser-api";
const logService = new ConsoleLogService(false); const logService = new ConsoleLogService(false);
if (BrowserApi.isManifestVersion(3)) {
startHeartbeat().catch((error) => logService.error(error));
}
const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); const bitwardenMain = ((self as any).bitwardenMain = new MainBackground());
bitwardenMain.bootstrap().catch((error) => logService.error(error)); bitwardenMain.bootstrap().catch((error) => logService.error(error));
/**
* Tracks when a service worker was last alive and extends the service worker
* lifetime by writing the current time to extension storage every 20 seconds.
*/
async function runHeartbeat() {
await chrome.storage.local.set({ "last-heartbeat": new Date().getTime() });
}
/**
* Starts the heartbeat interval which keeps the service worker alive.
*/
async function startHeartbeat() {
// Run the heartbeat once at service worker startup, then again every 20 seconds.
runHeartbeat()
.then(() => setInterval(runHeartbeat, 20 * 1000))
.catch((error) => logService.error(error));
}

View File

@ -58,11 +58,33 @@ export class BrowserApi {
} }
static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> { static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> {
return new Promise((resolve) => return new Promise((resolve) => {
chrome.windows.create(options, (window) => { chrome.windows.create(options, async (newWindow) => {
resolve(window); if (!BrowserApi.isSafariApi) {
}), return resolve(newWindow);
); }
// Safari doesn't close the default extension popup when a new window is created so we need to
// manually trigger the close by focusing the main window after the new window is created
const allWindows = await new Promise<chrome.windows.Window[]>((resolve) => {
chrome.windows.getAll({ windowTypes: ["normal"] }, (windows) => resolve(windows));
});
const mainWindow = allWindows.find((window) => window.id !== newWindow.id);
// No main window found, resolve the new window
if (mainWindow == null || !mainWindow.id) {
return resolve(newWindow);
}
// Focus the main window to close the extension popup
chrome.windows.update(mainWindow.id, { focused: true }, () => {
// Refocus the newly created window
chrome.windows.update(newWindow.id, { focused: true }, () => {
resolve(newWindow);
});
});
});
});
} }
/** /**

View File

@ -24,8 +24,8 @@ import {
LockComponentService, LockComponentService,
} from "@bitwarden/auth/angular"; } from "@bitwarden/auth/angular";
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common"; import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -92,9 +92,15 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service"; import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import { SyncService } from "@bitwarden/common/platform/sync"; import { SyncService } from "@bitwarden/common/platform/sync";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import {
FolderService as FolderServiceAbstraction,
InternalFolderService,
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
@ -107,7 +113,6 @@ import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extensio
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service";
import AutofillService from "../../autofill/services/autofill.service"; import AutofillService from "../../autofill/services/autofill.service";
import MainBackground from "../../background/main.background";
import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics"; import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics";
import { BrowserKeyService } from "../../key-management/browser-key.service"; import { BrowserKeyService } from "../../key-management/browser-key.service";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
@ -117,12 +122,12 @@ import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sen
/* eslint-enable no-restricted-imports */ /* eslint-enable no-restricted-imports */
import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document"; import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document";
import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service"; import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service";
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service"; import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service";
import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service";
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import BrowserMemoryStorageService from "../../platform/services/browser-memory-storage.service";
import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service";
import I18nService from "../../platform/services/i18n.service"; import I18nService from "../../platform/services/i18n.service";
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
@ -130,6 +135,7 @@ import { BrowserSdkClientFactory } from "../../platform/services/sdk/browser-sdk
import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service";
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { ForegroundSyncService } from "../../platform/sync/foreground-sync.service";
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ExtensionLockComponentService } from "../../services/extension-lock-component.service";
import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service";
@ -151,26 +157,6 @@ const DISK_BACKUP_LOCAL_STORAGE = new SafeInjectionToken<
AbstractStorageService & ObservableStorageService AbstractStorageService & ObservableStorageService
>("DISK_BACKUP_LOCAL_STORAGE"); >("DISK_BACKUP_LOCAL_STORAGE");
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
const mainBackground: MainBackground = needsBackgroundInit
? createLocalBgService()
: BrowserApi.getBackgroundPage().bitwardenMain;
function createLocalBgService() {
const localBgService = new MainBackground(true);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
localBgService.bootstrap();
return localBgService;
}
/** @deprecated This method needs to be removed as part of MV3 conversion. Please do not add more and actively try to remove usages */
function getBgService<T>(service: keyof MainBackground) {
return (): T => {
return mainBackground ? (mainBackground[service] as any as T) : null;
};
}
/** /**
* Provider definitions used in the ngModule. * Provider definitions used in the ngModule.
* Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety.
@ -307,8 +293,23 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: SyncService, provide: SyncService,
useFactory: getBgService<SyncService>("syncService"), useClass: ForegroundSyncService,
deps: [], deps: [
StateService,
InternalFolderService,
FolderApiServiceAbstraction,
MessageSender,
LogService,
CipherService,
CollectionService,
ApiService,
AccountServiceAbstraction,
AuthService,
InternalSendService,
SendApiService,
MessageListener,
StateProvider,
],
}), }),
safeProvider({ safeProvider({
provide: DomainSettingsService, provide: DomainSettingsService,
@ -358,11 +359,6 @@ const safeProviders: SafeProvider[] = [
useClass: ForegroundVaultTimeoutService, useClass: ForegroundVaultTimeoutService,
deps: [MessagingServiceAbstraction], deps: [MessagingServiceAbstraction],
}), }),
safeProvider({
provide: NotificationsService,
useFactory: getBgService<NotificationsService>("notificationsService"),
deps: [],
}),
safeProvider({ safeProvider({
provide: VaultFilterService, provide: VaultFilterService,
useClass: VaultFilterService, useClass: VaultFilterService,
@ -382,8 +378,8 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: MEMORY_STORAGE, provide: MEMORY_STORAGE,
useFactory: getBgService<AbstractStorageService>("memoryStorageService"), useFactory: (memoryStorage: AbstractStorageService) => memoryStorage,
deps: [], deps: [OBSERVABLE_MEMORY_STORAGE],
}), }),
safeProvider({ safeProvider({
provide: OBSERVABLE_MEMORY_STORAGE, provide: OBSERVABLE_MEMORY_STORAGE,
@ -392,9 +388,7 @@ const safeProviders: SafeProvider[] = [
return new ForegroundMemoryStorageService(); return new ForegroundMemoryStorageService();
} }
return getBgService<AbstractStorageService & ObservableStorageService>( return new BrowserMemoryStorageService();
"memoryStorageForStateProviders",
)();
}, },
deps: [], deps: [],
}), }),
@ -407,9 +401,7 @@ const safeProviders: SafeProvider[] = [
return regularMemoryStorageService; return regularMemoryStorageService;
} }
return getBgService<AbstractStorageService & ObservableStorageService>( return new ForegroundMemoryStorageService();
"largeObjectMemoryStorageForStateProviders",
)();
}, },
deps: [OBSERVABLE_MEMORY_STORAGE], deps: [OBSERVABLE_MEMORY_STORAGE],
}), }),
@ -494,15 +486,7 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT, provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => { useFactory: () => new Subject<Message<Record<string, unknown>>>(),
if (BrowserPopupUtils.backgroundInitializationRequired()) {
// There is no persistent main background which means we have one in memory,
// we need the same instance that our in memory background is utilizing.
return getBgService("intraprocessMessagingSubject")();
} else {
return new Subject<Message<Record<string, unknown>>>();
}
},
deps: [], deps: [],
}), }),
safeProvider({ safeProvider({
@ -514,23 +498,6 @@ const safeProviders: SafeProvider[] = [
), ),
deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService],
}), }),
safeProvider({
provide: INTRAPROCESS_MESSAGING_SUBJECT,
useFactory: () => {
if (needsBackgroundInit) {
// We will have created a popup within this context, in that case
// we want to make sure we have the same subject as that context so we
// can message with it.
return getBgService("intraprocessMessagingSubject")();
} else {
// There isn't a locally created background so we will communicate with
// the true background through chrome apis, in that case, we can just create
// one for ourself.
return new Subject<Message<Record<string, unknown>>>();
}
},
deps: [],
}),
safeProvider({ safeProvider({
provide: DISK_BACKUP_LOCAL_STORAGE, provide: DISK_BACKUP_LOCAL_STORAGE,
useFactory: (diskStorage: AbstractStorageService & ObservableStorageService) => useFactory: (diskStorage: AbstractStorageService & ObservableStorageService) =>
@ -572,13 +539,7 @@ const safeProviders: SafeProvider[] = [
}), }),
safeProvider({ safeProvider({
provide: ForegroundTaskSchedulerService, provide: ForegroundTaskSchedulerService,
useFactory: (logService: LogService, stateProvider: StateProvider) => { useClass: ForegroundTaskSchedulerService,
if (needsBackgroundInit) {
return getBgService<ForegroundTaskSchedulerService>("taskSchedulerService")();
}
return new ForegroundTaskSchedulerService(logService, stateProvider);
},
deps: [LogService, StateProvider], deps: [LogService, StateProvider],
}), }),
safeProvider({ safeProvider({

View File

@ -24,9 +24,9 @@ import {
SendFormConfig, SendFormConfig,
SendFormConfigService, SendFormConfigService,
SendFormMode, SendFormMode,
SendFormModule,
} from "@bitwarden/send-ui"; } from "@bitwarden/send-ui";
import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module";
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component"; import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";

View File

@ -118,7 +118,7 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Name" xml:space="preserve"> <data name="Name" xml:space="preserve">
<value>Bitwarden Password Manager</value> <value>Bitwarden passordbehandler</value>
</data> </data>
<data name="Summary" xml:space="preserve"> <data name="Summary" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value> <value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>

View File

@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/cli", "name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.10.0", "version": "2024.11.0",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",
@ -58,7 +58,7 @@
"dependencies": { "dependencies": {
"@koa/multer": "3.0.2", "@koa/multer": "3.0.2",
"@koa/router": "13.1.0", "@koa/router": "13.1.0",
"argon2": "0.40.1", "argon2": "0.41.1",
"big-integer": "1.6.52", "big-integer": "1.6.52",
"browser-hrtime": "1.1.8", "browser-hrtime": "1.1.8",
"chalk": "4.1.2", "chalk": "4.1.2",
@ -80,7 +80,7 @@
"papaparse": "5.4.1", "papaparse": "5.4.1",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tldts": "6.1.56", "tldts": "6.1.58",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
} }
} }

View File

@ -68,7 +68,7 @@ export class UnlockCommand {
return Response.error(e.message); return Response.error(e.message);
} }
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
await this.keyService.setUserKey(userKey, userId); await this.keyService.setUserKey(userKey, userId);
if (await this.keyConnectorService.getConvertAccountRequired()) { if (await this.keyConnectorService.getConvertAccountRequired()) {

View File

@ -404,6 +404,7 @@ export class ServiceContainer {
this.stateService, this.stateService,
this.keyGenerationService, this.keyGenerationService,
this.encryptService, this.encryptService,
this.logService,
); );
this.kdfConfigService = new KdfConfigService(this.stateProvider); this.kdfConfigService = new KdfConfigService(this.stateProvider);

View File

@ -4,18 +4,18 @@ version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.22.0" version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [ dependencies = [
"gimli", "gimli",
] ]
[[package]] [[package]]
name = "adler" name = "adler2"
version = "1.0.2" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "aes" name = "aes"
@ -39,9 +39,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.86" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]] [[package]]
name = "arboard" name = "arboard"
@ -216,17 +216,17 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.73" version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [ dependencies = [
"addr2line", "addr2line",
"cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide",
"object", "object",
"rustc-demangle", "rustc-demangle",
"windows-targets 0.52.6",
] ]
[[package]] [[package]]
@ -289,9 +289,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.7.2" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]] [[package]]
name = "cbc" name = "cbc"
@ -304,9 +304,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.28" version = "1.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -439,9 +439,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.128" version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54ccead7d199d584d139148b04b4a368d1ec7556a1d9ea2548febb1b9d49f9a4" checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007"
dependencies = [ dependencies = [
"cc", "cc",
"cxxbridge-flags", "cxxbridge-flags",
@ -451,9 +451,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx-build" name = "cxx-build"
version = "1.0.128" version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77953e99f01508f89f55c494bfa867171ef3a6c8cea03d26975368f2121a5c1" checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc"
dependencies = [ dependencies = [
"cc", "cc",
"codespan-reporting", "codespan-reporting",
@ -466,15 +466,15 @@ dependencies = [
[[package]] [[package]]
name = "cxxbridge-flags" name = "cxxbridge-flags"
version = "1.0.128" version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65777e06cc48f0cb0152024c77d6cf9e4bdb4408e7b48bea993d42fa0f5b02b6" checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff"
[[package]] [[package]]
name = "cxxbridge-macro" name = "cxxbridge-macro"
version = "1.0.128" version = "1.0.129"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98532a60dedaebc4848cb2cba5023337cc9ea3af16a5b062633fabfd9f18fb60" checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -546,6 +546,7 @@ dependencies = [
"napi-derive", "napi-derive",
"tokio", "tokio",
"tokio-util", "tokio-util",
"windows-registry",
] ]
[[package]] [[package]]
@ -758,9 +759,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "2.3.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"futures-core", "futures-core",
@ -843,9 +844,9 @@ dependencies = [
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.29.0" version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "gio" name = "gio"
@ -936,9 +937,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.0" version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
[[package]] [[package]]
name = "heck" name = "heck"
@ -964,15 +965,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.6.0" version = "2.6.0"
@ -1141,29 +1133,30 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.7.4" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [ dependencies = [
"adler", "adler2",
] ]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
dependencies = [ dependencies = [
"hermit-abi 0.3.9",
"libc", "libc",
"wasi", "wasi",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "napi" name = "napi"
version = "2.16.11" version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53575dfa17f208dd1ce3a2da2da4659aae393b256a472f2738a8586a6c4107fd" checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"ctor", "ctor",
@ -1258,16 +1251,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi 0.3.9",
"libc",
]
[[package]] [[package]]
name = "num_threads" name = "num_threads"
version = "0.1.7" version = "0.1.7"
@ -1458,9 +1441,9 @@ dependencies = [
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@ -1526,9 +1509,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.87" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1609,9 +1592,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.0" version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -1653,9 +1636,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.34" version = "0.38.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -1713,18 +1696,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.210" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.210" version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1832,9 +1815,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.79" version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1862,9 +1845,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.12.0" version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
@ -1884,18 +1867,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.61" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.61" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1937,26 +1920,25 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.38.0" version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"num_cpus",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.48.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.3.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2043,12 +2025,11 @@ dependencies = [
[[package]] [[package]]
name = "tree_magic_mini" name = "tree_magic_mini"
version = "3.1.5" version = "3.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63"
dependencies = [ dependencies = [
"fnv", "fnv",
"home",
"memchr", "memchr",
"nom", "nom",
"once_cell", "once_cell",
@ -2124,9 +2105,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-client" name = "wayland-client"
version = "0.31.6" version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d" checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"rustix", "rustix",
@ -2236,7 +2217,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-result", "windows-result 0.1.2",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@ -2262,6 +2243,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "windows-registry"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a"
dependencies = [
"windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.2" version = "0.1.2"
@ -2271,6 +2263,24 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@ -2477,9 +2487,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "4.3.1" version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40" checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-executor", "async-executor",
@ -2515,9 +2525,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "4.3.1" version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7" checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -2573,9 +2583,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "4.1.2" version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9" checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
dependencies = [ dependencies = [
"endi", "endi",
"enumflags2", "enumflags2",
@ -2586,9 +2596,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_derive" name = "zvariant_derive"
version = "4.1.2" version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859" checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -2599,9 +2609,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_utils" name = "zvariant_utils"
version = "2.0.0" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786" checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -23,7 +23,7 @@ sys = [
[dependencies] [dependencies]
aes = "=0.8.4" aes = "=0.8.4"
anyhow = "=1.0.86" anyhow = "=1.0.93"
arboard = { version = "=3.4.1", default-features = false, features = [ arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control", "wayland-data-control",
] } ] }
@ -38,8 +38,8 @@ rand = "=0.8.5"
retry = "=2.0.0" retry = "=2.0.0"
scopeguard = "=1.2.0" scopeguard = "=1.2.0"
sha2 = "=0.10.8" sha2 = "=0.10.8"
thiserror = "=1.0.61" thiserror = "=1.0.68"
tokio = { version = "=1.38.0", features = ["io-util", "sync", "macros"] } tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] }
tokio-util = "=0.7.12" tokio-util = "=0.7.12"
typenum = "=1.17.0" typenum = "=1.17.0"
@ -68,5 +68,5 @@ security-framework-sys = { version = "=2.11.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
gio = { version = "=0.19.5", optional = true } gio = { version = "=0.19.5", optional = true }
libsecret = { version = "=0.5.0", optional = true } libsecret = { version = "=0.5.0", optional = true }
zbus = { version = "=4.3.1", optional = true } zbus = { version = "=4.4.0", optional = true }
zbus_polkit = { version = "=4.0.0", optional = true } zbus_polkit = { version = "=4.0.0", optional = true }

View File

@ -31,7 +31,7 @@ pub fn path(name: &str) -> std::path::PathBuf {
format!(r"\\.\pipe\{hash_b64}.app.{name}").into() format!(r"\\.\pipe\{hash_b64}.app.{name}").into()
} }
#[cfg(target_os = "macos")] #[cfg(all(target_os = "macos", not(debug_assertions)))]
{ {
let mut home = dirs::home_dir().unwrap(); let mut home = dirs::home_dir().unwrap();
@ -53,6 +53,13 @@ pub fn path(name: &str) -> std::path::PathBuf {
tmp.join(format!("app.{name}")) tmp.join(format!("app.{name}"))
} }
#[cfg(all(target_os = "macos", debug_assertions))]
{
// When running in debug mode, we use the tmp dir because the app is not sandboxed
let dir = std::env::temp_dir();
dir.join(format!("app.{name}"))
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
// On Linux, we use the user's cache directory. // On Linux, we use the user's cache directory.

View File

@ -14,12 +14,15 @@ default = []
manual_test = [] manual_test = []
[dependencies] [dependencies]
anyhow = "=1.0.86" anyhow = "=1.0.93"
desktop_core = { path = "../core" } desktop_core = { path = "../core" }
napi = { version = "=2.16.11", features = ["async"] } napi = { version = "=2.16.13", features = ["async"] }
napi-derive = "=2.16.12" napi-derive = "=2.16.12"
tokio = { version = "1.38.0" } tokio = { version = "1.38.0" }
tokio-util = "0.7.11" tokio-util = "0.7.11"
[target.'cfg(windows)'.dependencies]
windows-registry = "=0.3.0"
[build-dependencies] [build-dependencies]
napi-build = "=2.1.3" napi-build = "=2.1.3"

View File

@ -51,6 +51,10 @@ export declare namespace powermonitors {
export function onLock(callback: (err: Error | null, ) => any): Promise<void> export function onLock(callback: (err: Error | null, ) => any): Promise<void>
export function isLockMonitorAvailable(): Promise<boolean> export function isLockMonitorAvailable(): Promise<boolean>
} }
export declare namespace windows_registry {
export function createKey(key: string, subkey: string, value: string): Promise<void>
export function deleteKey(key: string, subkey: string): Promise<void>
}
export declare namespace ipc { export declare namespace ipc {
export interface IpcMessage { export interface IpcMessage {
clientId: number clientId: number

View File

@ -1,5 +1,8 @@
#[macro_use] #[macro_use]
extern crate napi_derive; extern crate napi_derive;
mod registry;
#[napi] #[napi]
pub mod passwords { pub mod passwords {
/// Fetch the stored password from the keychain. /// Fetch the stored password from the keychain.
@ -190,6 +193,21 @@ pub mod powermonitors {
} }
#[napi]
pub mod windows_registry {
#[napi]
pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> {
crate::registry::create_key(&key, &subkey, &value)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> {
crate::registry::delete_key(&key, &subkey)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi] #[napi]
pub mod ipc { pub mod ipc {
use desktop_core::ipc::server::{Message, MessageType}; use desktop_core::ipc::server::{Message, MessageType};

View File

@ -0,0 +1,9 @@
use anyhow::{bail, Result};
pub fn create_key(_key: &str, _subkey: &str, _value: &str) -> Result<()> {
bail!("Not implemented")
}
pub fn delete_key(_key: &str, _subkey: &str) -> Result<()> {
bail!("Not implemented")
}

View File

@ -0,0 +1,4 @@
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(not(target_os = "windows"), path = "dummy.rs")]
mod internal;
pub use internal::*;

View File

@ -0,0 +1,29 @@
use anyhow::{bail, Result};
fn convert_key(key: &str) -> Result<&'static windows_registry::Key> {
Ok(match key.to_uppercase().as_str() {
"HKEY_CURRENT_USER" | "HKCU" => windows_registry::CURRENT_USER,
"HKEY_LOCAL_MACHINE" | "HKLM" => windows_registry::LOCAL_MACHINE,
"HKEY_CLASSES_ROOT" | "HKCR" => windows_registry::CLASSES_ROOT,
_ => bail!("Invalid key"),
})
}
pub fn create_key(key: &str, subkey: &str, value: &str) -> Result<()> {
let key = convert_key(key)?;
let subkey = key.create(subkey)?;
const DEFAULT: &str = "";
subkey.set_string(DEFAULT, value)?;
Ok(())
}
pub fn delete_key(key: &str, subkey: &str) -> Result<()> {
let key = convert_key(key)?;
key.remove_tree(subkey)?;
Ok(())
}

View File

@ -7,7 +7,7 @@ version = "0.0.0"
publish = false publish = false
[dependencies] [dependencies]
anyhow = "=1.0.86" anyhow = "=1.0.93"
desktop_core = { path = "../core", default-features = false } desktop_core = { path = "../core", default-features = false }
futures = "0.3.30" futures = "0.3.30"
log = "0.4.22" log = "0.4.22"

View File

@ -90,13 +90,6 @@
"electronUpdaterCompatibility": ">=0.0.1", "electronUpdaterCompatibility": ">=0.0.1",
"target": ["portable", "nsis-web", "appx"], "target": ["portable", "nsis-web", "appx"],
"sign": "./sign.js", "sign": "./sign.js",
"extraResources": [
{
"from": "../../node_modules/regedit/vbs",
"to": "regedit/vbs",
"filter": ["**/*"]
}
],
"extraFiles": [ "extraFiles": [
{ {
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",

View File

@ -125,9 +125,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.12.1", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"

View File

@ -219,7 +219,11 @@ export default class NativeMessageService {
key: string, key: string,
): Promise<DecryptedCommandData> { ): Promise<DecryptedCommandData> {
const sharedKey = await this.getSharedKeyForKey(key); const sharedKey = await this.getSharedKeyForKey(key);
const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey); const decrypted = await this.encryptService.decryptToUtf8(
payload,
sharedKey,
"native-messaging-session",
);
return JSON.parse(decrypted); return JSON.parse(decrypted);
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.10.3", "version": "2024.11.0",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",

View File

@ -58,20 +58,24 @@ async function run(context) {
id = identities[0].id; id = identities[0].id;
} }
console.log(`Signing proxy binary before the main bundle, using identity '${id}'`); console.log(
`Signing proxy binary before the main bundle, using identity '${id}', for build ${context.electronPlatformName}`,
);
const appName = context.packager.appInfo.productFilename; const appName = context.packager.appInfo.productFilename;
const appPath = `${context.appOutDir}/${appName}.app`; const appPath = `${context.appOutDir}/${appName}.app`;
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy"); const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const packageId = "com.bitwarden.desktop"; const packageId = "com.bitwarden.desktop";
if (is_mas) {
const entitlementsName = "entitlements.desktop_proxy.plist"; const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName); const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync( child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`, `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
); );
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist"; const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
const inheritEntitlementsPath = path.join( const inheritEntitlementsPath = path.join(
__dirname, __dirname,
@ -82,6 +86,18 @@ async function run(context) {
child_process.execSync( child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`, `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
); );
} else {
// For non-Appstore builds, we don't need the inherit binary as they are not sandboxed,
// but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly.
const entitlementsName = "entitlements.mac.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${inheritProxyPath}`,
);
}
} }
} }

View File

@ -626,7 +626,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
async saveBrowserIntegration() { async saveBrowserIntegration() {
if ( if (
ipc.platform.deviceType === DeviceType.MacOsDesktop && ipc.platform.deviceType === DeviceType.MacOsDesktop &&
!this.platformUtilsService.isMacAppStore() !this.platformUtilsService.isMacAppStore() &&
!ipc.platform.isDev
) { ) {
await this.dialogService.openSimpleDialog({ await this.dialogService.openSimpleDialog({
title: { key: "browserIntegrationUnsupportedTitle" }, title: { key: "browserIntegrationUnsupportedTitle" },

View File

@ -1,7 +1,7 @@
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs"; import { Subject, takeUntil, tap } from "rxjs";
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component"; import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
@ -14,8 +14,10 @@ import {
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -76,6 +78,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
webAuthnLoginService: WebAuthnLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction,
registerRouteService: RegisterRouteService, registerRouteService: RegisterRouteService,
toastService: ToastService, toastService: ToastService,
private configService: ConfigService,
) { ) {
super( super(
devicesApiService, devicesApiService,
@ -105,6 +108,8 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
} }
async ngOnInit() { async ngOnInit() {
this.listenForUnauthUiRefreshFlagChanges();
await super.ngOnInit(); await super.ngOnInit();
await this.getLoginWithDevice(this.loggedEmail); await this.getLoginWithDevice(this.loggedEmail);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
@ -137,6 +142,29 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
this.componentDestroyed$.complete(); this.componentDestroyed$.complete();
} }
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
async settings() { async settings() {
const [modal, childComponent] = await this.modalService.openViewRef( const [modal, childComponent] = await this.modalService.openViewRef(
EnvironmentComponent, EnvironmentComponent,

View File

@ -61,7 +61,7 @@
} }
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "Добродошли назад"
}, },
"moveToOrgDesc": { "moveToOrgDesc": {
"message": "Изаберите организацију у коју желите да преместите ову ставку. Пребацивање у организацију преноси власништво над ставком тој организацији. Више нећете бити директни власник ове ставке када буде премештена." "message": "Изаберите организацију у коју желите да преместите ову ставку. Пребацивање у организацију преноси власништво над ставком тој организацији. Више нећете бити директни власник ове ставке када буде премештена."
@ -263,7 +263,7 @@
"message": "Генерисање лозинке" "message": "Генерисање лозинке"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "Генеришите приступну фразу"
}, },
"type": { "type": {
"message": "Тип" "message": "Тип"
@ -401,7 +401,7 @@
"message": "Копирај лозинку" "message": "Копирај лозинку"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase", "message": "Копирај приступну фразу",
"description": "Copy passphrase to clipboard" "description": "Copy passphrase to clipboard"
}, },
"copyUri": { "copyUri": {
@ -558,7 +558,7 @@
"message": "Креирај налог" "message": "Креирај налог"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "Нови сте у Bitwarden-у?"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "Поставите јаку лозинку" "message": "Поставите јаку лозинку"
@ -570,16 +570,16 @@
"message": "Пријавите се" "message": "Пријавите се"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "Пријавите се на Bitwarden"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "Пријавите се са приступним кључем"
}, },
"loginWithDevice": { "loginWithDevice": {
"message": "Log in with device" "message": "Пријавите се са уређајем"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "Употребити једнократну пријаву"
}, },
"submit": { "submit": {
"message": "Пошаљи" "message": "Пошаљи"
@ -854,7 +854,7 @@
"message": "УРЛ Сервера" "message": "УРЛ Сервера"
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "УРЛ сервера који се самостално хостује",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@ -1248,7 +1248,7 @@
"description": "Copy credit card number" "description": "Copy credit card number"
}, },
"copyEmail": { "copyEmail": {
"message": "Copy email" "message": "Копирати имејл"
}, },
"copySecurityCode": { "copySecurityCode": {
"message": "Копирај сигурносни код", "message": "Копирај сигурносни код",
@ -1684,10 +1684,10 @@
"message": "Брисање налога је трајно. Не може се поништити." "message": "Брисање налога је трајно. Не може се поништити."
}, },
"cannotDeleteAccount": { "cannotDeleteAccount": {
"message": "Cannot delete account" "message": "Није могуће избрисати налог"
}, },
"cannotDeleteAccountDesc": { "cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." "message": "Ова радња се не може довршити јер је ваш налог у власништву организације. Обратите се администратору своје организације за додатне детаље."
}, },
"accountDeleted": { "accountDeleted": {
"message": "Налог обрисан" "message": "Налог обрисан"
@ -2391,10 +2391,10 @@
"message": "Генериши име" "message": "Генериши име"
}, },
"generateEmail": { "generateEmail": {
"message": "Generate email" "message": "Генеришите имејл"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$", "message": "Вредност мора бити између $MIN$ и $MAX$",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@ -2451,11 +2451,11 @@
"message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања." "message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања."
}, },
"forwarderDomainName": { "forwarderDomainName": {
"message": "Email domain", "message": "Домен имејла",
"description": "Labels the domain name email forwarder service option" "description": "Labels the domain name email forwarder service option"
}, },
"forwarderDomainNameHint": { "forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service", "message": "Изаберите домен који подржава изабрана услуга",
"description": "Guidance provided for email forwarding services that support multiple email domains." "description": "Guidance provided for email forwarding services that support multiple email domains."
}, },
"forwarderError": { "forwarderError": {
@ -2944,10 +2944,10 @@
"message": "Приступачни кључ" "message": "Приступачни кључ"
}, },
"passkeyNotCopied": { "passkeyNotCopied": {
"message": "Приступачни кључ се неће копирати" "message": "Приступни кључ неће бити копиран"
}, },
"passkeyNotCopiedAlert": { "passkeyNotCopiedAlert": {
"message": "Приступачни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?" "message": "Приступни кључ неће бити копиран на клонирану ставку. Да ли желите да наставите са клонирањем ставке?"
}, },
"aliasDomain": { "aliasDomain": {
"message": "Домен алијаса" "message": "Домен алијаса"
@ -3168,10 +3168,10 @@
"message": "Омогућите хардверско убрзање и поново покрените" "message": "Омогућите хардверско убрзање и поново покрените"
}, },
"removePasskey": { "removePasskey": {
"message": "Уклонити приступачни кључ" "message": "Уклонити приступни кључ"
}, },
"passkeyRemoved": { "passkeyRemoved": {
"message": "Приступачни кључ је уклоњен" "message": "Приступни кључ је уклоњен"
}, },
"errorAssigningTargetCollection": { "errorAssigningTargetCollection": {
"message": "Грешка при додељивању циљне колекције." "message": "Грешка при додељивању циљне колекције."

View File

@ -61,7 +61,7 @@
} }
}, },
"welcomeBack": { "welcomeBack": {
"message": "Welcome back" "message": "欢迎回来"
}, },
"moveToOrgDesc": { "moveToOrgDesc": {
"message": "选择一个您想将此项目移至的组织。移动到组织会将该项目的所有权转让给该组织。移动后,您将不再是此项目的直接所有者。" "message": "选择一个您想将此项目移至的组织。移动到组织会将该项目的所有权转让给该组织。移动后,您将不再是此项目的直接所有者。"
@ -263,7 +263,7 @@
"message": "生成密码" "message": "生成密码"
}, },
"generatePassphrase": { "generatePassphrase": {
"message": "Generate passphrase" "message": "生成密码短语"
}, },
"type": { "type": {
"message": "类型" "message": "类型"
@ -401,7 +401,7 @@
"message": "复制密码" "message": "复制密码"
}, },
"copyPassphrase": { "copyPassphrase": {
"message": "Copy passphrase", "message": "复制密码短语",
"description": "Copy passphrase to clipboard" "description": "Copy passphrase to clipboard"
}, },
"copyUri": { "copyUri": {
@ -558,7 +558,7 @@
"message": "创建账户" "message": "创建账户"
}, },
"newToBitwarden": { "newToBitwarden": {
"message": "New to Bitwarden?" "message": "Bitwarden 新手吗?"
}, },
"setAStrongPassword": { "setAStrongPassword": {
"message": "设置强密码" "message": "设置强密码"
@ -570,16 +570,16 @@
"message": "登录" "message": "登录"
}, },
"logInToBitwarden": { "logInToBitwarden": {
"message": "Log in to Bitwarden" "message": "登录到 Bitwarden"
}, },
"logInWithPasskey": { "logInWithPasskey": {
"message": "Log in with passkey" "message": "使用通行密钥登录"
}, },
"loginWithDevice": { "loginWithDevice": {
"message": "Log in with device" "message": "使用设备登录"
}, },
"useSingleSignOn": { "useSingleSignOn": {
"message": "Use single sign-on" "message": "使用单点登录"
}, },
"submit": { "submit": {
"message": "提交" "message": "提交"
@ -854,7 +854,7 @@
"message": "服务器 URL" "message": "服务器 URL"
}, },
"selfHostBaseUrl": { "selfHostBaseUrl": {
"message": "Self-host server URL", "message": "自托管服务器 URL",
"description": "Label for field requesting a self-hosted integration service URL" "description": "Label for field requesting a self-hosted integration service URL"
}, },
"apiUrl": { "apiUrl": {
@ -1248,7 +1248,7 @@
"description": "Copy credit card number" "description": "Copy credit card number"
}, },
"copyEmail": { "copyEmail": {
"message": "Copy email" "message": "复制电子邮件地址"
}, },
"copySecurityCode": { "copySecurityCode": {
"message": "复制安全码", "message": "复制安全码",
@ -1684,10 +1684,10 @@
"message": "删除账户是永久性操作,无法撤销!" "message": "删除账户是永久性操作,无法撤销!"
}, },
"cannotDeleteAccount": { "cannotDeleteAccount": {
"message": "Cannot delete account" "message": "无法删除账户"
}, },
"cannotDeleteAccountDesc": { "cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details." "message": "此操作无法完成,因为您的账户归组织所有。请联系您的组织管理员获取详细信息。"
}, },
"accountDeleted": { "accountDeleted": {
"message": "账户已删除" "message": "账户已删除"
@ -2391,10 +2391,10 @@
"message": "生成用户名" "message": "生成用户名"
}, },
"generateEmail": { "generateEmail": {
"message": "Generate email" "message": "生成邮件地址"
}, },
"generatorBoundariesHint": { "generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$", "message": "值必须在 $MIN$ 和 $MAX$ 之间",
"description": "Explains spin box minimum and maximum values to the user", "description": "Explains spin box minimum and maximum values to the user",
"placeholders": { "placeholders": {
"min": { "min": {
@ -2451,11 +2451,11 @@
"message": "使用外部转发服务生成一个电子邮件别名。" "message": "使用外部转发服务生成一个电子邮件别名。"
}, },
"forwarderDomainName": { "forwarderDomainName": {
"message": "Email domain", "message": "邮件域名",
"description": "Labels the domain name email forwarder service option" "description": "Labels the domain name email forwarder service option"
}, },
"forwarderDomainNameHint": { "forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service", "message": "选择一个所选服务支持的域名",
"description": "Guidance provided for email forwarding services that support multiple email domains." "description": "Guidance provided for email forwarding services that support multiple email domains."
}, },
"forwarderError": { "forwarderError": {

View File

@ -1,12 +1,11 @@
import { existsSync, promises as fs } from "fs"; import { existsSync, promises as fs } from "fs";
import { homedir, userInfo } from "os"; import { homedir, userInfo } from "os";
import * as path from "path"; import * as path from "path";
import * as util from "util";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ipc } from "@bitwarden/desktop-napi"; import { ipc, windows_registry } from "@bitwarden/desktop-napi";
import { isDev } from "../utils"; import { isDev } from "../utils";
@ -132,18 +131,7 @@ export class NativeMessagingMain {
}; };
const chromeJson = { const chromeJson = {
...baseJson, ...baseJson,
...{ allowed_origins: await this.loadChromeIds(),
allowed_origins: [
// Chrome extension
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
// Chrome beta extension
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
// Edge extension
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
// Opera extension
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/",
],
},
}; };
switch (process.platform) { switch (process.platform) {
@ -153,12 +141,12 @@ export class NativeMessagingMain {
await this.writeManifest(path.join(destination, "chrome.json"), chromeJson); await this.writeManifest(path.join(destination, "chrome.json"), chromeJson);
const nmhs = this.getWindowsNMHS(); const nmhs = this.getWindowsNMHS();
for (const [key, value] of Object.entries(nmhs)) { for (const [name, [key, subkey]] of Object.entries(nmhs)) {
let manifestPath = path.join(destination, "chrome.json"); let manifestPath = path.join(destination, "chrome.json");
if (key === "Firefox") { if (name === "Firefox") {
manifestPath = path.join(destination, "firefox.json"); manifestPath = path.join(destination, "firefox.json");
} }
await this.createWindowsRegistry(value, manifestPath); await windows_registry.createKey(key, subkey, manifestPath);
} }
break; break;
} }
@ -180,35 +168,26 @@ export class NativeMessagingMain {
} }
break; break;
} }
case "linux": case "linux": {
if (existsSync(`${this.homedir()}/.mozilla/`)) { for (const [key, value] of Object.entries(this.getLinuxNMHS())) {
if (existsSync(value)) {
if (key === "Firefox") {
await this.writeManifest( await this.writeManifest(
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
firefoxJson, firefoxJson,
); );
} } else {
if (existsSync(`${this.homedir()}/.config/google-chrome/`)) {
await this.writeManifest( await this.writeManifest(
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"),
chromeJson, chromeJson,
); );
} }
} else {
if (existsSync(`${this.homedir()}/.config/microsoft-edge/`)) { this.logService.warning(`${key} not found, skipping.`);
await this.writeManifest(
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`,
chromeJson,
);
} }
if (existsSync(`${this.homedir()}/.config/chromium/`)) {
await this.writeManifest(
`${this.homedir()}/.config/chromium/NativeMessagingHosts/com.8bit.bitwarden.json`,
chromeJson,
);
} }
break; break;
}
default: default:
break; break;
} }
@ -245,8 +224,8 @@ export class NativeMessagingMain {
await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json")); await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json"));
const nmhs = this.getWindowsNMHS(); const nmhs = this.getWindowsNMHS();
for (const [, value] of Object.entries(nmhs)) { for (const [, [key, subkey]] of Object.entries(nmhs)) {
await this.deleteWindowsRegistry(value); await windows_registry.deleteKey(key, subkey);
} }
break; break;
} }
@ -260,15 +239,18 @@ export class NativeMessagingMain {
break; break;
} }
case "linux": { case "linux": {
for (const [key, value] of Object.entries(this.getLinuxNMHS())) {
if (key === "Firefox") {
await this.removeIfExists( await this.removeIfExists(
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
); );
} else {
await this.removeIfExists( await this.removeIfExists(
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"),
);
await this.removeIfExists(
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`,
); );
}
}
break; break;
} }
default: default:
@ -291,11 +273,14 @@ export class NativeMessagingMain {
private getWindowsNMHS() { private getWindowsNMHS() {
return { return {
Firefox: "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", Firefox: ["HKCU", "SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden"],
Chrome: "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", Chrome: ["HKCU", "SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden"],
Chromium: "HKCU\\SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden", Chromium: ["HKCU", "SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden"],
// Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well. // Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well.
"Microsoft Edge": "HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden", "Microsoft Edge": [
"HKCU",
"SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden",
],
}; };
} }
@ -317,6 +302,15 @@ export class NativeMessagingMain {
/* eslint-enable no-useless-escape */ /* eslint-enable no-useless-escape */
} }
private getLinuxNMHS() {
return {
Firefox: `${this.homedir()}/.mozilla/`,
Chrome: `${this.homedir()}/.config/google-chrome/`,
Chromium: `${this.homedir()}/.config/chromium/`,
"Microsoft Edge": `${this.homedir()}/.config/microsoft-edge/`,
};
}
private async writeManifest(destination: string, manifest: object) { private async writeManifest(destination: string, manifest: object) {
this.logService.debug(`Writing manifest: ${destination}`); this.logService.debug(`Writing manifest: ${destination}`);
@ -327,6 +321,83 @@ export class NativeMessagingMain {
await fs.writeFile(destination, JSON.stringify(manifest, null, 2)); await fs.writeFile(destination, JSON.stringify(manifest, null, 2));
} }
private async loadChromeIds(): Promise<string[]> {
const ids: Set<string> = new Set([
// Chrome extension
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
// Chrome beta extension
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
// Edge extension
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
// Opera extension
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/",
]);
if (!isDev()) {
return Array.from(ids);
}
// The dev builds of the extension have a different random ID per user, so to make development easier
// we try to find the extension IDs from the user's Chrome profiles when we're running in dev mode.
let chromePaths: string[];
switch (process.platform) {
case "darwin": {
chromePaths = Object.entries(this.getDarwinNMHS())
.filter(([key]) => key !== "Firefox")
.map(([, value]) => value);
break;
}
case "linux": {
chromePaths = Object.entries(this.getLinuxNMHS())
.filter(([key]) => key !== "Firefox")
.map(([, value]) => value);
break;
}
case "win32": {
// TODO: Add more supported browsers for Windows?
chromePaths = [
path.join(process.env.LOCALAPPDATA, "Microsoft", "Edge", "User Data"),
path.join(process.env.LOCALAPPDATA, "Google", "Chrome", "User Data"),
];
break;
}
}
for (const chromePath of chromePaths) {
try {
// The chrome profile directories are named "Default", "Profile 1", "Profile 2", etc.
const profiles = (await fs.readdir(chromePath)).filter((f) => {
const lower = f.toLowerCase();
return lower == "default" || lower.startsWith("profile ");
});
for (const profile of profiles) {
try {
// Read the profile Preferences file and find the extension commands section
const prefs = JSON.parse(
await fs.readFile(path.join(chromePath, profile, "Preferences"), "utf8"),
);
const commands: Map<string, any> = prefs.extensions.commands;
// If one of the commands is autofill_login or generate_password, we know it's probably the Bitwarden extension
for (const { command_name, extension } of Object.values(commands)) {
if (command_name === "autofill_login" || command_name === "generate_password") {
ids.add(`chrome-extension://${extension}/`);
this.logService.info(`Found extension from ${chromePath}: ${extension}`);
}
}
} catch (e) {
this.logService.info(`Error reading preferences: ${e}`);
}
}
} catch (e) {
// Browser is not installed, we can just skip it
}
}
return Array.from(ids);
}
private binaryPath() { private binaryPath() {
const ext = process.platform === "win32" ? ".exe" : ""; const ext = process.platform === "win32" ? ".exe" : "";
@ -350,52 +421,6 @@ export class NativeMessagingMain {
return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`); return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
} }
private getRegeditInstance() {
// eslint-disable-next-line
const regedit = require("regedit");
regedit.setExternalVBSLocation(path.join(path.dirname(this.exePath), "resources/regedit/vbs"));
return regedit;
}
private async createWindowsRegistry(location: string, jsonFile: string) {
const regedit = this.getRegeditInstance();
const createKey = util.promisify(regedit.createKey);
const putValue = util.promisify(regedit.putValue);
this.logService.debug(`Adding registry: ${location}`);
await createKey(location);
// Insert path to manifest
const obj: any = {};
obj[location] = {
default: {
value: jsonFile,
type: "REG_DEFAULT",
},
};
return putValue(obj);
}
private async deleteWindowsRegistry(key: string) {
const regedit = this.getRegeditInstance();
const list = util.promisify(regedit.list);
const deleteKey = util.promisify(regedit.deleteKey);
this.logService.debug(`Removing registry: ${key}`);
try {
await list(key);
await deleteKey(key);
} catch {
this.logService.error(`Unable to delete registry key: ${key}`);
}
}
private homedir() { private homedir() {
if (process.platform === "darwin") { if (process.platform === "darwin") {
return userInfo().homedir; return userInfo().homedir;

View File

@ -1,16 +1,16 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.10.3", "version": "2024.11.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.10.3", "version": "2024.11.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi", "@bitwarden/desktop-napi": "file:../desktop_native/napi",
"argon2": "0.40.1" "argon2": "0.41.1"
} }
}, },
"../desktop_native/napi": { "../desktop_native/napi": {
@ -35,25 +35,28 @@
} }
}, },
"node_modules/argon2": { "node_modules/argon2": {
"version": "0.40.1", "version": "0.41.1",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.1.tgz", "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz",
"integrity": "sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==", "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@phc/format": "^1.0.0", "@phc/format": "^1.0.0",
"node-addon-api": "^7.1.0", "node-addon-api": "^8.1.0",
"node-gyp-build": "^4.8.0" "node-gyp-build": "^4.8.1"
}, },
"engines": { "engines": {
"node": ">=16.17.0" "node": ">=16.17.0"
} }
}, },
"node_modules/node-addon-api": { "node_modules/node-addon-api": {
"version": "7.1.1", "version": "8.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-vmEOvxwiH8tlOcv4SyE8RH34rI5/nWVaigUeAUPawC6f0+HoDthwI0vkMu4tbtsZrXq6QXFfrkhjofzKEs5tpA==",
"license": "MIT" "license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
}, },
"node_modules/node-gyp-build": { "node_modules/node-gyp-build": {
"version": "4.8.2", "version": "4.8.2",

View File

@ -2,7 +2,7 @@
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"productName": "Bitwarden", "productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.10.3", "version": "2024.11.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",
@ -13,6 +13,6 @@
}, },
"dependencies": { "dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi", "@bitwarden/desktop-napi": "file:../desktop_native/napi",
"argon2": "0.40.1" "argon2": "0.41.1"
} }
} }

View File

@ -185,6 +185,7 @@ export class NativeMessageHandlerService {
let decryptedResult = await this.encryptService.decryptToUtf8( let decryptedResult = await this.encryptService.decryptToUtf8(
message.encryptedCommand as EncString, message.encryptedCommand as EncString,
this.ddgSharedSecret, this.ddgSharedSecret,
"ddg-shared-key",
); );
decryptedResult = this.trimNullCharsFromMessage(decryptedResult); decryptedResult = this.trimNullCharsFromMessage(decryptedResult);

View File

@ -114,6 +114,7 @@ export class NativeMessagingService {
await this.encryptService.decryptToUtf8( await this.encryptService.decryptToUtf8(
rawMessage as EncString, rawMessage as EncString,
SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)), SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)),
`native-messaging-session-${appId}`,
), ),
); );

View File

@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/web-vault", "name": "@bitwarden/web-vault",
"version": "2024.10.5", "version": "2024.11.0",
"scripts": { "scripts": {
"build:oss": "webpack", "build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@ -40,9 +40,9 @@
></bit-nav-item> ></bit-nav-item>
</bit-nav-group> </bit-nav-group>
<bit-nav-item <bit-nav-item
*ngIf="isAccessIntelligenceFeatureEnabled" *ngIf="isRiskInsightsFeatureEnabled"
[text]="'accessIntelligence' | i18n" [text]="'riskInsights' | i18n"
route="access-intelligence" route="risk-insights"
></bit-nav-item> ></bit-nav-item>
<bit-nav-group <bit-nav-group
icon="bwi-billing" icon="bwi-billing"

View File

@ -51,7 +51,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
showPaymentAndHistory$: Observable<boolean>; showPaymentAndHistory$: Observable<boolean>;
hideNewOrgButton$: Observable<boolean>; hideNewOrgButton$: Observable<boolean>;
organizationIsUnmanaged$: Observable<boolean>; organizationIsUnmanaged$: Observable<boolean>;
isAccessIntelligenceFeatureEnabled = false; isRiskInsightsFeatureEnabled = false;
private _destroy = new Subject<void>(); private _destroy = new Subject<void>();
@ -71,7 +71,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
async ngOnInit() { async ngOnInit() {
document.body.classList.remove("layout_frontend"); document.body.classList.remove("layout_frontend");
this.isAccessIntelligenceFeatureEnabled = await this.configService.getFeatureFlag( this.isRiskInsightsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccessIntelligence, FeatureFlag.AccessIntelligence,
); );

View File

@ -23,15 +23,14 @@
<bit-tab [label]="'role' | i18n"> <bit-tab [label]="'role' | i18n">
<ng-container *ngIf="!editMode"> <ng-container *ngIf="!editMode">
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p> <p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
<bit-form-field *ngIf="remainingSeats$ | async as remainingSeats"> <bit-form-field>
<bit-label>{{ "email" | i18n }}</bit-label> <bit-label>{{ "email" | i18n }}</bit-label>
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" /> <input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
<bit-hint *ngIf="remainingSeats > 1; else singleSeat">{{ <bit-hint>{{
"inviteMultipleEmailDesc" | i18n: remainingSeats "inviteMultipleEmailDesc"
| i18n
: (organization.productTierType === ProductTierType.TeamsStarter ? "10" : "20")
}}</bit-hint> }}</bit-hint>
<ng-template #singleSeat>
<bit-hint>{{ "inviteSingleEmailDesc" | i18n: remainingSeats }}</bit-hint>
</ng-template>
</bit-form-field> </bit-form-field>
</ng-container> </ng-container>
<bit-radio-group formControlName="type"> <bit-radio-group formControlName="type">
@ -265,6 +264,16 @@
<button <button
*ngIf="editMode" *ngIf="editMode"
type="button" type="button"
bitIconButton="bwi-close"
buttonType="danger"
bitFormButton
[appA11yTitle]="'remove' | i18n"
[bitAction]="remove"
[disabled]="loading"
></button>
<button
*ngIf="editMode && params.managedByOrganization === true"
type="button"
bitIconButton="bwi-trash" bitIconButton="bwi-trash"
buttonType="danger" buttonType="danger"
bitFormButton bitFormButton

View File

@ -65,6 +65,7 @@ export interface MemberDialogParams {
isOnSecretsManagerStandalone: boolean; isOnSecretsManagerStandalone: boolean;
initialTab?: MemberDialogTab; initialTab?: MemberDialogTab;
numConfirmedMembers: number; numConfirmedMembers: number;
managedByOrganization?: boolean;
} }
export enum MemberDialogResult { export enum MemberDialogResult {
@ -89,7 +90,6 @@ export class MemberDialogComponent implements OnDestroy {
PermissionMode = PermissionMode; PermissionMode = PermissionMode;
showNoMasterPasswordWarning = false; showNoMasterPasswordWarning = false;
isOnSecretsManagerStandalone: boolean; isOnSecretsManagerStandalone: boolean;
remainingSeats$: Observable<number>;
protected organization$: Observable<Organization>; protected organization$: Observable<Organization>;
protected collectionAccessItems: AccessItemView[] = []; protected collectionAccessItems: AccessItemView[] = [];
@ -251,10 +251,6 @@ export class MemberDialogComponent implements OnDestroy {
this.loading = false; this.loading = false;
}); });
this.remainingSeats$ = this.organization$.pipe(
map((organization) => organization.seats - this.params.numConfirmedMembers),
);
} }
private setFormValidators(organization: Organization) { private setFormValidators(organization: Organization) {
@ -469,7 +465,7 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Saved); this.close(MemberDialogResult.Saved);
}; };
delete = async () => { remove = async () => {
if (!this.editMode) { if (!this.editMode) {
return; return;
} }
@ -566,6 +562,39 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Restored); this.close(MemberDialogResult.Restored);
}; };
delete = async () => {
if (!this.editMode) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.params.name],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
await this.organizationUserApiService.deleteOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.params.name),
});
this.close(MemberDialogResult.Deleted);
};
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();

View File

@ -320,6 +320,17 @@
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }} <i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span> </span>
</button> </button>
<button
*ngIf="u.managedByOrganization === true"
type="button"
bitMenuItem
(click)="deleteUser(u)"
>
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
</bit-menu> </bit-menu>
</td> </td>
</tr> </tr>

View File

@ -486,7 +486,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
const enableUpgradePasswordManagerSub = await firstValueFrom( const enableUpgradePasswordManagerSub = await firstValueFrom(
this.enableUpgradePasswordManagerSub$, this.enableUpgradePasswordManagerSub$,
); );
if (enableUpgradePasswordManagerSub) { if (enableUpgradePasswordManagerSub && this.organization.canEditSubscription) {
const reference = openChangePlanDialog(this.dialogService, { const reference = openChangePlanDialog(this.dialogService, {
data: { data: {
organizationId: this.organization.id, organizationId: this.organization.id,
@ -518,6 +518,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: initialTab, initialTab: initialTab,
numConfirmedMembers: this.dataSource.confirmedUserCount, numConfirmedMembers: this.dataSource.confirmedUserCount,
managedByOrganization: user?.managedByOrganization,
}, },
}); });
@ -725,6 +726,40 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return true; return true;
} }
async deleteUser(user: OrganizationUserView) {
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.userNamePipe.transform(user)],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id,
user.id,
);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
});
this.dataSource.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) { private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
return this.dialogService.openSimpleDialog({ return this.dialogService.openSimpleDialog({
title: { title: {

View File

@ -63,10 +63,10 @@ const routes: Routes = [
), ),
}, },
{ {
path: "access-intelligence", path: "risk-insights",
loadChildren: () => loadChildren: () =>
import("../../tools/access-intelligence/access-intelligence.module").then( import("../../tools/risk-insights/risk-insights.module").then(
(m) => m.AccessIntelligenceModule, (m) => m.RiskInsightsModule,
), ),
}, },
{ {

View File

@ -123,21 +123,23 @@ export class AccountComponent implements OnInit, OnDestroy {
this.canEditSubscription = organization.canEditSubscription; this.canEditSubscription = organization.canEditSubscription;
this.canUseApi = organization.useApi; this.canUseApi = organization.useApi;
// Update disabled states - reactive forms prefers not using disabled attribute
// Disabling these fields for self hosted orgs is deprecated // Disabling these fields for self hosted orgs is deprecated
// This block can be completely removed as part of // This block can be completely removed as part of
// https://bitwarden.atlassian.net/browse/PM-10863 // https://bitwarden.atlassian.net/browse/PM-10863
if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) { if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
if (!this.selfHosted) { if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable(); this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable();
this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable(); this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable();
} }
} }
if (!this.selfHosted && this.canEditSubscription) { // Update disabled states - reactive forms prefers not using disabled attribute
if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
if (this.canEditSubscription) {
this.formGroup.get("billingEmail").enable(); this.formGroup.get("billingEmail").enable();
} }
}
// Org Response // Org Response
this.org = orgResponse; this.org = orgResponse;

View File

@ -21,7 +21,13 @@
> >
{{ "purgeVault" | i18n }} {{ "purgeVault" | i18n }}
</button> </button>
<button type="button" bitButton buttonType="danger" [bitAction]="deleteAccount"> <button
*ngIf="showDeleteAccount$ | async"
type="button"
bitButton
buttonType="danger"
[bitAction]="deleteAccount"
>
{{ "deleteAccount" | i18n }} {{ "deleteAccount" | i18n }}
</button> </button>
</app-danger-zone> </app-danger-zone>

View File

@ -24,6 +24,7 @@ export class AccountComponent implements OnInit {
showChangeEmail$: Observable<boolean>; showChangeEmail$: Observable<boolean>;
showPurgeVault$: Observable<boolean>; showPurgeVault$: Observable<boolean>;
showDeleteAccount$: Observable<boolean>;
constructor( constructor(
private modalService: ModalService, private modalService: ModalService,
@ -64,6 +65,16 @@ export class AccountComponent implements OnInit {
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization, !isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
), ),
); );
this.showDeleteAccount$ = combineLatest([
isAccountDeprovisioningEnabled$,
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
} }
async deauthorizeSessions() { async deauthorizeSessions() {

View File

@ -1,22 +1,33 @@
import { CommonModule } from "@angular/common";
import { importProvidersFrom } from "@angular/core"; import { importProvidersFrom } from "@angular/core";
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { ButtonModule } from "@bitwarden/components"; import { ButtonModule } from "@bitwarden/components";
import { PreloadedEnglishI18nModule } from "../../../core/tests"; import { PreloadedEnglishI18nModule } from "../../../core/tests";
import { DangerZoneComponent } from "./danger-zone.component"; import { DangerZoneComponent } from "./danger-zone.component";
class MockConfigService implements Partial<ConfigService> {}
export default { export default {
title: "Web/Danger Zone", title: "Web/Danger Zone",
component: DangerZoneComponent, component: DangerZoneComponent,
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [ButtonModule, JslibModule], imports: [ButtonModule, JslibModule, CommonModule],
}), }),
applicationConfig({ applicationConfig({
providers: [importProvidersFrom(PreloadedEnglishI18nModule)], providers: [
importProvidersFrom(PreloadedEnglishI18nModule),
{
provide: ConfigService,
useClass: MockConfigService,
multi: true,
},
],
}), }),
], ],
} as Meta; } as Meta;

View File

@ -194,7 +194,7 @@ export class ChangePasswordComponent
HashPurpose.LocalAuthorization, HashPurpose.LocalAuthorization,
); );
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey, userId);
if (userKey == null) { if (userKey == null) {
this.toastService.showToast({ this.toastService.showToast({
variant: "error", variant: "error",

View File

@ -48,16 +48,7 @@
}}</span> }}</span>
</dd> </dd>
<dt>{{ "nextCharge" | i18n }}</dt> <dt>{{ "nextCharge" | i18n }}</dt>
<dd *ngIf="!enableTimeThreshold"> <dd>
{{
nextInvoice
? (nextInvoice.date | date: "mediumDate") +
", " +
(nextInvoice.amount | currency: "$")
: "-"
}}
</dd>
<dd *ngIf="enableTimeThreshold">
{{ {{
nextInvoice nextInvoice
? (sub.subscription.periodEndDate | date: "mediumDate") + ? (sub.subscription.periodEndDate | date: "mediumDate") +

View File

@ -38,13 +38,9 @@ export class UserSubscriptionComponent implements OnInit {
sub: SubscriptionResponse; sub: SubscriptionResponse;
selfHosted = false; selfHosted = false;
cloudWebVaultUrl: string; cloudWebVaultUrl: string;
enableTimeThreshold: boolean;
cancelPromise: Promise<any>; cancelPromise: Promise<any>;
reinstatePromise: Promise<any>; reinstatePromise: Promise<any>;
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
FeatureFlag.AC2476_DeprecateStripeSourcesAPI, FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
@ -69,7 +65,6 @@ export class UserSubscriptionComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
await this.load(); await this.load();
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
this.firstLoaded = true; this.firstLoaded = true;
} }

View File

@ -48,10 +48,7 @@
<dt [ngClass]="{ 'tw-text-danger': isExpired }"> <dt [ngClass]="{ 'tw-text-danger': isExpired }">
{{ "subscriptionExpiration" | i18n }} {{ "subscriptionExpiration" | i18n }}
</dt> </dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="!enableTimeThreshold"> <dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
</dd>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="enableTimeThreshold">
{{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }} {{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }}
</dd> </dd>
</ng-container> </ng-container>

View File

@ -52,7 +52,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
loading = true; loading = true;
locale: string; locale: string;
showUpdatedSubscriptionStatusSection$: Observable<boolean>; showUpdatedSubscriptionStatusSection$: Observable<boolean>;
enableTimeThreshold: boolean;
preSelectedProductTier: ProductTierType = ProductTierType.Free; preSelectedProductTier: ProductTierType = ProductTierType.Free;
showSubscription = true; showSubscription = true;
showSelfHost = false; showSelfHost = false;
@ -65,10 +64,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.EnableConsolidatedBilling, FeatureFlag.EnableConsolidatedBilling,
); );
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableUpgradePasswordManagerSub, FeatureFlag.EnableUpgradePasswordManagerSub,
); );
@ -117,7 +112,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
); );
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
} }
ngOnDestroy() { ngOnDestroy() {
@ -298,9 +292,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString()); return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString());
} }
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) { } else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
}
const seatAdjustmentMessage = this.sub.plan.isAnnual const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage" ? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage"; : "monthlySubscriptionUserSeatsMessage";
@ -311,21 +302,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
} else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) { } else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) {
return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10); return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10);
} else if (this.sub.maxAutoscaleSeats == null) { } else if (this.sub.maxAutoscaleSeats == null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale");
}
const seatAdjustmentMessage = this.sub.plan.isAnnual const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage" ? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage"; : "monthlySubscriptionUserSeatsMessage";
return this.i18nService.t(seatAdjustmentMessage); return this.i18nService.t(seatAdjustmentMessage);
} else { } else {
if (!this.enableTimeThreshold) {
return this.i18nService.t(
"subscriptionUserSeatsLimitedAutoscale",
this.sub.maxAutoscaleSeats.toString(),
);
}
const seatAdjustmentMessage = this.sub.plan.isAnnual const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage" ? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage"; : "monthlySubscriptionUserSeatsMessage";

View File

@ -1,9 +0,0 @@
import { NgModule } from "@angular/core";
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
import { AccessIntelligenceComponent } from "./access-intelligence.component";
@NgModule({
imports: [AccessIntelligenceComponent, AccessIntelligenceRoutingModule],
})
export class AccessIntelligenceModule {}

View File

@ -1,11 +0,0 @@
<!-- <bit-table [dataSource]="dataSource"> -->
<ng-container header>
<tr>
<th bitCell>{{ "application" | i18n }}</th>
<th bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitCell>{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<!-- </bit-table> -->

View File

@ -1,19 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { TableDataSource, TableModule } from "@bitwarden/components";
@Component({
standalone: true,
selector: "tools-application-table",
templateUrl: "./application-table.component.html",
imports: [CommonModule, JslibModule, TableModule],
})
export class ApplicationTableComponent {
protected dataSource = new TableDataSource<any>();
constructor() {
this.dataSource.data = [];
}
}

View File

@ -1,15 +0,0 @@
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noPriorityApplicationsTitle" | i18n }}
</h2>
</ng-container>
<ng-container slot="description">
<p class="tw-text-muted">
{{ "noPriorityApplicationsDescription" | i18n }}
</p>
</ng-container>
<ng-container slot="button">
<button bitButton buttonType="primary" type="button">{{ "markPriorityApps" | i18n }}</button>
</ng-container>
</bit-no-items>

View File

@ -1,15 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ButtonModule, NoItemsModule, Icons } from "@bitwarden/components";
@Component({
standalone: true,
selector: "tools-no-priority-apps",
templateUrl: "no-priority-apps.component.html",
imports: [ButtonModule, CommonModule, JslibModule, NoItemsModule],
})
export class NoPriorityAppsComponent {
noItemsIcon = Icons.Security;
}

View File

@ -1,123 +0,0 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<tools-no-priority-apps></tools-no-priority-apps>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button class="tw-rounded-lg" type="button" buttonType="secondary" bitButton>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
[disabled]="!selectedIds.size"
bitButton
[bitAction]="markAppsAsCritical"
appA11yTitle="{{ 'markAppAsCritical' | i18n }}"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>
</div>

View File

@ -0,0 +1,113 @@
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noAppsInOrgTitle" | i18n: organization.name }}
</h2>
</ng-container>
<ng-container slot="description">
<div class="tw-flex tw-flex-col tw-mb-2">
<span class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
</span>
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
</ng-container>
<ng-container slot="button">
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
{{ "createNewLoginItem" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="mockAtRiskMembersCount"
[maxValue]="mockTotalMembersCount"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="mockAtRiskAppsCount"
[maxValue]="mockTotalAppsCount"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
bitButton
[disabled]="!selectedIds.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<span>{{ r.name }}</span>
</td>
<td bitCell>
<span>
{{ r.atRiskPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.totalPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.atRiskMembers }}
</span>
</td>
<td bitCell data-testid="total-membership">
{{ r.totalMembers }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@ -0,0 +1,118 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { debounceTime, firstValueFrom, map } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
Icons,
NoItemsModule,
SearchModule,
TableDataSource,
ToastService,
} from "@bitwarden/components";
import { CardComponent } from "@bitwarden/tools-card";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { applicationTableMockData } from "./application-table.mock";
@Component({
standalone: true,
selector: "tools-all-applications",
templateUrl: "./all-applications.component.html",
imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule],
})
export class AllApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<any>();
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected organization: Organization;
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
// MOCK DATA
protected mockData = applicationTableMockData;
protected mockAtRiskMembersCount = 0;
protected mockAtRiskAppsCount = 0;
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
const organizationId = params.get("organizationId");
this.organization = await firstValueFrom(this.organizationService.get$(organizationId));
// TODO: use organizationId to fetch data
}),
)
.subscribe();
}
constructor(
protected cipherService: CipherService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected auditService: AuditService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected organizationService: OrganizationService,
) {
this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
goToCreateNewLoginItem = async () => {
// TODO: implement
this.toastService.showToast({
variant: "warning",
title: null,
message: "Not yet implemented",
});
};
markAppsAsCritical = async () => {
// TODO: Send to API once implemented
this.markingAsCritical = true;
return new Promise((resolve) => {
setTimeout(() => {
this.selectedIds.clear();
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("appsMarkedAsCritical"),
});
resolve(true);
this.markingAsCritical = false;
}, 1000);
});
};
trackByFunction(_: number, item: CipherView) {
return item.id;
}
onCheckboxChange(id: number, event: Event) {
const isChecked = (event.target as HTMLInputElement).checked;
if (isChecked) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
}
}

View File

@ -0,0 +1,50 @@
export const applicationTableMockData = [
{
id: 1,
name: "google.com",
atRiskPasswords: 4,
totalPasswords: 10,
atRiskMembers: 2,
totalMembers: 5,
},
{
id: 2,
name: "facebook.com",
atRiskPasswords: 3,
totalPasswords: 8,
atRiskMembers: 1,
totalMembers: 3,
},
{
id: 3,
name: "twitter.com",
atRiskPasswords: 2,
totalPasswords: 6,
atRiskMembers: 0,
totalMembers: 2,
},
{
id: 4,
name: "linkedin.com",
atRiskPasswords: 1,
totalPasswords: 4,
atRiskMembers: 0,
totalMembers: 1,
},
{
id: 5,
name: "instagram.com",
atRiskPasswords: 0,
totalPasswords: 2,
atRiskMembers: 0,
totalMembers: 0,
},
{
id: 6,
name: "tiktok.com",
atRiskPasswords: 0,
totalPasswords: 1,
atRiskMembers: 0,
totalMembers: 0,
},
];

View File

@ -0,0 +1,99 @@
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noCriticalAppsTitle" | i18n }}
</h2>
</ng-container>
<ng-container slot="description">
<p class="tw-text-muted">
{{ "noCriticalAppsDescription" | i18n }}
</p>
</ng-container>
<ng-container slot="button">
<button (click)="goToAllAppsTab()" bitButton buttonType="primary" type="button">
{{ "markCriticalApps" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-justify-between tw-mb-4">
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
<button bitButton buttonType="primary" type="button">
<i class="bwi bwi-envelope tw-mr-2"></i>
{{ "requestPasswordChange" | i18n }}
</button>
</div>
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="mockAtRiskMembersCount"
[maxValue]="mockTotalMembersCount"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="mockAtRiskAppsCount"
[maxValue]="mockTotalAppsCount"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td>
<i class="bwi bwi-star-f"></i>
</td>
<td bitCell>
<span>{{ r.name }}</span>
</td>
<td bitCell>
<span>
{{ r.atRiskPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.totalPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.atRiskMembers }}
</span>
</td>
<td bitCell data-testid="total-membership">
{{ r.totalMembers }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@ -0,0 +1,68 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { debounceTime, map } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SearchModule, TableDataSource, NoItemsModule, Icons } from "@bitwarden/components";
import { CardComponent } from "@bitwarden/tools-card";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { applicationTableMockData } from "./application-table.mock";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
standalone: true,
selector: "tools-critical-applications",
templateUrl: "./critical-applications.component.html",
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
})
export class CriticalApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<any>();
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected organizationId: string;
noItemsIcon = Icons.Security;
// MOCK DATA
protected mockData = applicationTableMockData;
protected mockAtRiskMembersCount = 0;
protected mockAtRiskAppsCount = 0;
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
this.organizationId = params.get("organizationId");
// TODO: use organizationId to fetch data
}),
)
.subscribe();
}
goToAllAppsTab = async () => {
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
queryParamsHandling: "merge",
});
};
constructor(
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected router: Router,
) {
this.dataSource.data = []; //applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
}

View File

@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs"; import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@ -6,7 +6,7 @@ import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";

View File

@ -0,0 +1,64 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@ -5,7 +5,7 @@ import { ActivatedRoute } from "@angular/router";
import { debounceTime, map } from "rxjs"; import { debounceTime, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence"; import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
@ -27,8 +27,6 @@ import { OrganizationBadgeModule } from "../../vault/individual-vault/organizati
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { NoPriorityAppsComponent } from "./no-priority-apps.component";
@Component({ @Component({
standalone: true, standalone: true,
selector: "tools-password-health-members", selector: "tools-password-health-members",
@ -40,7 +38,6 @@ import { NoPriorityAppsComponent } from "./no-priority-apps.component";
HeaderModule, HeaderModule,
SearchModule, SearchModule,
FormsModule, FormsModule,
NoPriorityAppsComponent,
SharedModule, SharedModule,
TableModule, TableModule,
], ],
@ -100,7 +97,7 @@ export class PasswordHealthMembersComponent implements OnInit {
await passwordHealthService.generateReport(); await passwordHealthService.generateReport();
this.dataSource.data = []; //passwordHealthService.reportCiphers; this.dataSource.data = passwordHealthService.reportCiphers;
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap; this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap; this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;

Some files were not shown because too many files have changed in this diff Show More