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,7 +219,11 @@ export class AutofillComponent implements OnInit {
: AutofillOverlayVisibility.Off; : AutofillOverlayVisibility.Off;
await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue); await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue);
await this.requestPrivacyPermission();
// No need to initiate browser permission request if a feature is being turned off
if (newInlineMenuVisibilityValue !== AutofillOverlayVisibility.Off) {
await this.requestPrivacyPermission();
}
} }
async updateAutofillOnPageLoad() { async updateAutofillOnPageLoad() {

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,45 +457,6 @@ export default class MainBackground {
this.offscreenDocumentService, this.offscreenDocumentService,
); );
// Creates a session key for mv3 storage of large memory items
const sessionKey = new Lazy(async () => {
// Key already in session storage
const sessionStorage = new BrowserMemoryStorageService();
const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key");
if (existingKey) {
if (sessionStorage.valuesRequireDeserialization) {
return SymmetricCryptoKey.fromJSON(existingKey);
}
return existingKey;
}
// New key
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
await sessionStorage.save("session-key", derivedKey);
return derivedKey;
});
const mv3MemoryStorageCreator = () => {
if (this.popupOnlyContext) {
return new ForegroundMemoryStorageService();
}
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory
// and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation`
// so that MAC failures are not logged.
return new LocalBackedSessionStorageService(
sessionKey,
this.storageService,
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
this.platformUtilsService,
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 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)) { if (BrowserApi.isManifestVersion(3)) {
@ -506,18 +464,47 @@ export default class MainBackground {
this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session
this.memoryStorageService = this.memoryStorageForStateProviders; this.memoryStorageService = this.memoryStorageForStateProviders;
} else { } else {
if (popupOnlyContext) { this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageForStateProviders = new ForegroundMemoryStorageService(); this.memoryStorageService = this.memoryStorageForStateProviders;
this.memoryStorageService = new ForegroundMemoryStorageService();
} else {
this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageService = this.memoryStorageForStateProviders;
}
} }
this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) if (BrowserApi.isManifestVersion(3)) {
? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage // Creates a session key for mv3 storage of large memory items
: this.memoryStorageForStateProviders; // mv2 stores to the same location const sessionKey = new Lazy(async () => {
// Key already in session storage
const sessionStorage = new BrowserMemoryStorageService();
const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key");
if (existingKey) {
if (sessionStorage.valuesRequireDeserialization) {
return SymmetricCryptoKey.fromJSON(existingKey);
}
return existingKey;
}
// New key
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
await sessionStorage.save("session-key", derivedKey);
return derivedKey;
});
this.largeObjectMemoryStorageForStateProviders = new LocalBackedSessionStorageService(
sessionKey,
this.storageService,
// For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory
// and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation`
// so that MAC failures are not logged.
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
this.platformUtilsService,
this.logService,
);
} else {
// mv2 stores to the same location
this.largeObjectMemoryStorageForStateProviders = this.memoryStorageForStateProviders;
}
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,26 +861,24 @@ 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, this.cipherService,
this.cipherService, this.folderService,
this.folderService, this.collectionService,
this.collectionService, this.platformUtilsService,
this.platformUtilsService, this.messagingService,
this.messagingService, this.searchService,
this.searchService, this.stateService,
this.stateService, this.authService,
this.authService, this.vaultTimeoutSettingsService,
this.vaultTimeoutSettingsService, this.stateEventRunnerService,
this.stateEventRunnerService, this.taskSchedulerService,
this.taskSchedulerService, this.logService,
this.logService, 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,59 +899,41 @@ export default class MainBackground {
this.providerService = new ProviderService(this.stateProvider); this.providerService = new ProviderService(this.stateProvider);
if (this.popupOnlyContext) { this.syncService = new DefaultSyncService(
this.syncService = new ForegroundSyncService( this.masterPasswordService,
this.stateService, this.accountService,
this.folderService, this.apiService,
this.folderApiService, this.domainSettingsService,
this.messagingService, this.folderService,
this.logService, this.cipherService,
this.cipherService, this.keyService,
this.collectionService, this.collectionService,
this.apiService, this.messagingService,
this.accountService, this.policyService,
this.authService, this.sendService,
this.sendService, this.logService,
this.sendApiService, this.keyConnectorService,
messageListener, this.stateService,
this.stateProvider, this.providerService,
); this.folderApiService,
} else { this.organizationService,
this.syncService = new DefaultSyncService( this.sendApiService,
this.masterPasswordService, this.userDecryptionOptionsService,
this.accountService, this.avatarService,
this.apiService, logoutCallback,
this.domainSettingsService, this.billingAccountProfileStateService,
this.folderService, this.tokenService,
this.cipherService, this.authService,
this.keyService, this.stateProvider,
this.collectionService, );
this.messagingService,
this.policyService, this.syncServiceListener = new SyncServiceListener(
this.sendService, this.syncService,
this.logService, messageListener,
this.keyConnectorService, this.messagingService,
this.stateService, this.logService,
this.providerService, );
this.folderApiService,
this.organizationService,
this.sendApiService,
this.userDecryptionOptionsService,
this.avatarService,
logoutCallback,
this.billingAccountProfileStateService,
this.tokenService,
this.authService,
this.stateProvider,
);
this.syncServiceListener = new SyncServiceListener(
this.syncService,
messageListener,
this.messagingService,
this.logService,
);
}
this.eventUploadService = new EventUploadService( this.eventUploadService = new EventUploadService(
this.apiService, this.apiService,
this.stateProvider, this.stateProvider,
@ -1111,122 +1080,128 @@ export default class MainBackground {
this.isSafari = this.platformUtilsService.isSafari(); this.isSafari = this.platformUtilsService.isSafari();
// Background // Background
if (!this.popupOnlyContext) {
this.fido2Background = new Fido2Background(
this.logService,
this.fido2ActiveRequestManager,
this.fido2ClientService,
this.vaultSettingsService,
this.scriptInjectorService,
this.configService,
this.authService,
);
const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService); this.fido2Background = new Fido2Background(
this.logService,
this.fido2ActiveRequestManager,
this.fido2ClientService,
this.vaultSettingsService,
this.scriptInjectorService,
this.configService,
this.authService,
);
this.runtimeBackground = new RuntimeBackground( const lockService = new DefaultLockService(this.accountService, this.vaultTimeoutService);
this,
this.autofillService,
this.platformUtilsService as BrowserPlatformUtilsService,
this.notificationsService,
this.autofillSettingsService,
this.processReloadService,
this.environmentService,
this.messagingService,
this.logService,
this.configService,
messageListener,
this.accountService,
lockService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.keyService,
this.encryptService,
this.cryptoFunctionService,
this.runtimeBackground,
this.messagingService,
this.appIdService,
this.platformUtilsService,
this.logService,
this.authService,
this.biometricStateService,
this.accountService,
);
this.commandsBackground = new CommandsBackground(
this,
this.platformUtilsService,
this.vaultTimeoutService,
this.authService,
() => this.generatePasswordToClipboard(),
);
this.notificationBackground = new NotificationBackground(
this.autofillService,
this.cipherService,
this.authService,
this.policyService,
this.folderService,
this.userNotificationSettingsService,
this.domainSettingsService,
this.environmentService,
this.logService,
this.themeStateService,
this.configService,
this.accountService,
);
this.overlayNotificationsBackground = new OverlayNotificationsBackground( this.runtimeBackground = new RuntimeBackground(
this.logService, this,
this.configService, this.autofillService,
this.notificationBackground, this.platformUtilsService as BrowserPlatformUtilsService,
); this.notificationsService,
this.autofillSettingsService,
this.processReloadService,
this.environmentService,
this.messagingService,
this.logService,
this.configService,
messageListener,
this.accountService,
lockService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.keyService,
this.encryptService,
this.cryptoFunctionService,
this.runtimeBackground,
this.messagingService,
this.appIdService,
this.platformUtilsService,
this.logService,
this.authService,
this.biometricStateService,
this.accountService,
);
this.commandsBackground = new CommandsBackground(
this,
this.platformUtilsService,
this.vaultTimeoutService,
this.authService,
() => this.generatePasswordToClipboard(),
);
this.notificationBackground = new NotificationBackground(
this.autofillService,
this.cipherService,
this.authService,
this.policyService,
this.folderService,
this.userNotificationSettingsService,
this.domainSettingsService,
this.environmentService,
this.logService,
this.themeStateService,
this.configService,
this.accountService,
);
this.filelessImporterBackground = new FilelessImporterBackground( this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.configService, this.logService,
this.authService, this.configService,
this.policyService, this.notificationBackground,
this.notificationBackground, );
this.importService,
this.syncService,
this.scriptInjectorService,
);
this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( this.filelessImporterBackground = new FilelessImporterBackground(
this.logService, this.configService,
this.autofillService, this.authService,
this.scriptInjectorService, this.policyService,
this.authService, this.notificationBackground,
this.configService, this.importService,
this.platformUtilsService, this.syncService,
this.policyService, this.scriptInjectorService,
); );
const contextMenuClickedHandler = new ContextMenuClickedHandler( this.autoSubmitLoginBackground = new AutoSubmitLoginBackground(
(options) => this.platformUtilsService.copyToClipboard(options.text), this.logService,
async () => this.generatePasswordToClipboard(), this.autofillService,
async (tab, cipher) => { this.scriptInjectorService,
this.loginToAutoFill = cipher; this.authService,
if (tab == null) { this.configService,
return; this.platformUtilsService,
} this.policyService,
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. const contextMenuClickedHandler = new ContextMenuClickedHandler(
// eslint-disable-next-line @typescript-eslint/no-floating-promises (options) => this.platformUtilsService.copyToClipboard(options.text),
BrowserApi.tabSendMessage(tab, { async (_tab) => {
command: "collectPageDetails", const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
tab: tab, const password = await this.passwordGenerationService.generatePassword(options);
sender: "contextMenu", 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.authService, this.passwordGenerationService.addHistory(password);
this.cipherService, },
this.totpService, async (tab, cipher) => {
this.eventCollectionService, this.loginToAutoFill = cipher;
this.userVerificationService, if (tab == null) {
this.accountService, return;
); }
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
} // eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.tabSendMessage(tab, {
command: "collectPageDetails",
tab: tab,
sender: "contextMenu",
});
},
this.authService,
this.cipherService,
this.totpService,
this.eventCollectionService,
this.userVerificationService,
this.accountService,
);
this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler);
this.idleBackground = new IdleBackground( this.idleBackground = new IdleBackground(
this.vaultTimeoutService, this.vaultTimeoutService,
@ -1245,29 +1220,27 @@ 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, this.i18nService,
this.i18nService, this.logService,
this.logService, this.billingAccountProfileStateService,
this.billingAccountProfileStateService, );
);
this.cipherContextMenuHandler = new CipherContextMenuHandler( this.cipherContextMenuHandler = new CipherContextMenuHandler(
this.mainContextMenuHandler, this.mainContextMenuHandler,
this.authService, this.authService,
this.cipherService,
);
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
this.webRequestBackground = new WebRequestBackground(
this.platformUtilsService,
this.cipherService, this.cipherService,
this.authService,
chrome.webRequest,
); );
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
this.webRequestBackground = new WebRequestBackground(
this.platformUtilsService,
this.cipherService,
this.authService,
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,30 +58,46 @@ 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";
const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit"); if (is_mas) {
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist"; const entitlementsName = "entitlements.desktop_proxy.plist";
const inheritEntitlementsPath = path.join( const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
__dirname, child_process.execSync(
"..", `codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
"resources", );
inheritEntitlementsName,
); const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
child_process.execSync( const inheritEntitlementsPath = path.join(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`, __dirname,
); "..",
"resources",
inheritEntitlementsName,
);
child_process.execSync(
`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())) {
await this.writeManifest( if (existsSync(value)) {
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, if (key === "Firefox") {
firefoxJson, await this.writeManifest(
); path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
} firefoxJson,
);
if (existsSync(`${this.homedir()}/.config/google-chrome/`)) { } else {
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": {
await this.removeIfExists( for (const [key, value] of Object.entries(this.getLinuxNMHS())) {
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, if (key === "Firefox") {
); await this.removeIfExists(
await this.removeIfExists( path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, );
); } else {
await this.removeIfExists( await this.removeIfExists(
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, path.join(value, "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,20 +123,22 @@ 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
this.formGroup.get("billingEmail").enable(); if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
if (this.canEditSubscription) {
this.formGroup.get("billingEmail").enable();
}
} }
// Org Response // Org Response

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