From 279632d65f1e8dfe1af0a1d2fdce5c11bd5637c1 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 21 Nov 2025 12:10:03 -0500 Subject: [PATCH 01/44] [PM-28516] Inline menu is not working in main (#17524) * PM-28516 alidate iframe and stylesheet URLs against their own origins to handle cases where chrome assigns different extension ids in different contexts * switch to regex to match exisiting match pattern * updated regex to account for safari --- .../autofill/background/overlay.background.ts | 17 ++++++---- .../autofill-inline-menu-container.ts | 1 + .../autofill-inline-menu-container.spec.ts | 34 +++++++++++++++++++ .../autofill-inline-menu-container.ts | 23 ++++++++----- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 225cbbe66ca..0eb7d070de3 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -2949,17 +2949,21 @@ export class OverlayBackground implements OverlayBackgroundInterface { (await this.checkFocusedFieldHasValue(port.sender.tab)) && (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)); + const iframeUrl = chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, + ); + const styleSheetUrl = chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, + ); + const extensionOrigin = new URL(iframeUrl).origin; + this.postMessageToPort(port, { command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, - iframeUrl: chrome.runtime.getURL( - `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, - ), + iframeUrl, pageTitle: chrome.i18n.getMessage( isInlineMenuListPort ? "bitwardenVault" : "bitwardenOverlayButton", ), - styleSheetUrl: chrome.runtime.getURL( - `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, - ), + styleSheetUrl, theme: await firstValueFrom(this.themeStateService.selectedTheme$), translations: this.getInlineMenuTranslations(), ciphers: isInlineMenuListPort ? await this.getInlineMenuCipherData() : null, @@ -2973,6 +2977,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { showSaveLoginMenu, showInlineMenuAccountCreation, authStatus, + extensionOrigin, }); this.updateInlineMenuPosition( port.sender, diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts index af60d1de77d..64fa8dde124 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts @@ -17,6 +17,7 @@ export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMe translations: Record; ciphers: InlineMenuCipherData[] | null; portName: string; + extensionOrigin?: string; }; export type AutofillInlineMenuContainerWindowMessage = AutofillInlineMenuContainerMessage & diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts index d7a61bec61f..e0a6e626b3c 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.spec.ts @@ -184,4 +184,38 @@ describe("AutofillInlineMenuContainer", () => { expect(port.postMessage).not.toHaveBeenCalled(); }); }); + + describe("isExtensionUrlWithOrigin", () => { + it("validates extension URLs with matching origin", () => { + const url = "chrome-extension://test-id/path/to/file.html"; + const origin = "chrome-extension://test-id"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(true); + }); + + it("rejects extension URLs with mismatched origin", () => { + const url = "chrome-extension://test-id/path/to/file.html"; + const origin = "chrome-extension://different-id"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(false); + }); + + it("validates extension URL against its own origin when no expectedOrigin provided", () => { + const url = "moz-extension://test-id/path/to/file.html"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url)).toBe(true); + }); + + it("rejects non-extension protocols", () => { + const url = "https://example.com/path"; + const origin = "https://example.com"; + + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(false); + }); + + it("rejects empty or invalid URLs", () => { + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"]("")).toBe(false); + expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"]("not-a-url")).toBe(false); + }); + }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts index ad0b11f0bc6..aea6ef30b99 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts @@ -87,11 +87,13 @@ export class AutofillInlineMenuContainer { return; } - if (!this.isExtensionUrl(message.iframeUrl)) { + const expectedOrigin = message.extensionOrigin || this.extensionOrigin; + + if (!this.isExtensionUrlWithOrigin(message.iframeUrl, expectedOrigin)) { return; } - if (message.styleSheetUrl && !this.isExtensionUrl(message.styleSheetUrl)) { + if (message.styleSheetUrl && !this.isExtensionUrlWithOrigin(message.styleSheetUrl)) { return; } @@ -115,20 +117,25 @@ export class AutofillInlineMenuContainer { } /** - * validates that a URL is from the extension origin. - * prevents loading arbitrary URLs in the iframe. + * Validates that a URL uses an extension protocol and matches the expected extension origin. + * If no expectedOrigin is provided, validates against the URL's own origin. * * @param url - The URL to validate. */ - private isExtensionUrl(url: string): boolean { + private isExtensionUrlWithOrigin(url: string, expectedOrigin?: string): boolean { if (!url) { return false; } try { const urlObj = new URL(url); - return ( - urlObj.origin === this.extensionOrigin || urlObj.href.startsWith(this.extensionOrigin + "/") - ); + const isExtensionProtocol = /^[a-z]+(-[a-z]+)?-extension:$/i.test(urlObj.protocol); + + if (!isExtensionProtocol) { + return false; + } + + const originToValidate = expectedOrigin ?? urlObj.origin; + return urlObj.origin === originToValidate || urlObj.href.startsWith(originToValidate + "/"); } catch { return false; } From 129c21cfb87d7352a2f059154aa8908a9414baf3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:23:51 -0500 Subject: [PATCH 02/44] [deps] Vault: Update koa to v2.16.3 [SECURITY] (#17514) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 10 +++++----- package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index fc38440b70f..a7a7bfd0c7b 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -75,7 +75,7 @@ "inquirer": "8.2.6", "jsdom": "26.1.0", "jszip": "3.10.1", - "koa": "2.16.2", + "koa": "2.16.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", diff --git a/package-lock.json b/package-lock.json index d6883a3405e..005eb38d80b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "inquirer": "8.2.6", "jsdom": "26.1.0", "jszip": "3.10.1", - "koa": "2.16.2", + "koa": "2.16.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lit": "3.3.0", @@ -213,7 +213,7 @@ "inquirer": "8.2.6", "jsdom": "26.1.0", "jszip": "3.10.1", - "koa": "2.16.2", + "koa": "2.16.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lowdb": "1.0.0", @@ -28074,9 +28074,9 @@ } }, "node_modules/koa": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.2.tgz", - "integrity": "sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", + "integrity": "sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==", "license": "MIT", "dependencies": { "accepts": "^1.3.5", diff --git a/package.json b/package.json index 73ba1c05c91..7ca866b3c4d 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "inquirer": "8.2.6", "jsdom": "26.1.0", "jszip": "3.10.1", - "koa": "2.16.2", + "koa": "2.16.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", "lit": "3.3.0", From aa2d2637518aa049fd07272f0d830b970826e897 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:48:50 -0600 Subject: [PATCH 03/44] [PM-24505] Manually open extension error message (#17116) * update manual open message to be more generic to cover more scenarios * update error state when attempting to open the extension via button press --- ...browser-extension-prompt.component.spec.ts | 4 +-- .../browser-extension-prompt.component.ts | 5 ++-- .../manually-open-extension.component.html | 8 ++--- .../manually-open-extension.component.ts | 5 ++-- .../setup-extension.component.html | 30 ++++++++----------- .../setup-extension.component.spec.ts | 13 -------- .../setup-extension.component.ts | 24 +++++++-------- apps/web/src/locales/en/messages.json | 23 +++++++------- 8 files changed, 44 insertions(+), 68 deletions(-) diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.spec.ts b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.spec.ts index a19606f6d9c..b31759a1fe1 100644 --- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.spec.ts +++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.spec.ts @@ -166,8 +166,8 @@ describe("BrowserExtensionPromptComponent", () => { it("shows manual open error message", () => { const manualText = fixture.debugElement.query(By.css("p")).nativeElement; - expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart1"); - expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart2"); + expect(manualText.textContent.trim()).toContain("openExtensionFromToolbarPart1"); + expect(manualText.textContent.trim()).toContain("openExtensionFromToolbarPart2"); }); }); }); diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts index cb927d0848c..505a0df5032 100644 --- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts +++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts @@ -1,5 +1,5 @@ import { CommonModule, DOCUMENT } from "@angular/common"; -import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { Component, Inject, OnDestroy, OnInit, ChangeDetectionStrategy } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; import { map, Observable, of, tap } from "rxjs"; @@ -14,12 +14,11 @@ import { } from "../../services/browser-extension-prompt.service"; import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manually-open-extension.component"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "vault-browser-extension-prompt", templateUrl: "./browser-extension-prompt.component.html", imports: [CommonModule, I18nPipe, ButtonComponent, IconModule, ManuallyOpenExtensionComponent], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class BrowserExtensionPromptComponent implements OnInit, OnDestroy { protected VaultMessages = VaultMessages; diff --git a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.html b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.html index 22c36e51177..d15cdaa712b 100644 --- a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.html +++ b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.html @@ -1,8 +1,8 @@ -

- {{ "openExtensionManuallyPart1" | i18n }} +

+ {{ "openExtensionFromToolbarPart1" | i18n }} - {{ "openExtensionManuallyPart2" | i18n }} + {{ "openExtensionFromToolbarPart2" | i18n }}

diff --git a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts index 6105aeacf9c..435e847f6e9 100644 --- a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts +++ b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts @@ -1,12 +1,11 @@ -import { Component } from "@angular/core"; +import { Component, ChangeDetectionStrategy } from "@angular/core"; import { BitwardenIcon } from "@bitwarden/assets/svg"; import { IconModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: "vault-manually-open-extension", templateUrl: "./manually-open-extension.component.html", imports: [I18nPipe, IconModule], diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html index 038c258d4b6..1976321b4ee 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html @@ -29,10 +29,7 @@ -
+
@@ -40,20 +37,15 @@ {{ (state === SetupExtensionState.Success ? "bitwardenExtensionInstalled" - : "openTheBitwardenExtension" + : "bitwardenExtensionIsInstalled" ) | i18n }}

- {{ - (state === SetupExtensionState.Success - ? "openExtensionToAutofill" - : "bitwardenExtensionInstalledOpenExtension" - ) | i18n - }} + {{ "openExtensionToAutofill" | i18n }}

-

+ +

{{ "gettingStartedWithBitwardenPart1" | i18n }} {{ "gettingStartedWithBitwardenPart2" | i18n }} @@ -73,7 +73,3 @@

- -
- -
diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts index fbf61f9a277..ef67e072116 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts @@ -4,7 +4,6 @@ import { Router, RouterModule } from "@angular/router"; import { BehaviorSubject } from "rxjs"; import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens"; -import { BrowserExtensionIcon } from "@bitwarden/assets/svg"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -12,7 +11,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { StateProvider } from "@bitwarden/common/platform/state"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; -import { AnonLayoutWrapperDataService } from "@bitwarden/components"; import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; @@ -25,14 +23,12 @@ describe("SetupExtensionComponent", () => { const navigate = jest.fn().mockResolvedValue(true); const openExtension = jest.fn().mockResolvedValue(true); const update = jest.fn().mockResolvedValue(true); - const setAnonLayoutWrapperData = jest.fn(); const extensionInstalled$ = new BehaviorSubject(null); beforeEach(async () => { navigate.mockClear(); openExtension.mockClear(); update.mockClear(); - setAnonLayoutWrapperData.mockClear(); window.matchMedia = jest.fn().mockReturnValue(false); await TestBed.configureTestingModule({ @@ -43,7 +39,6 @@ describe("SetupExtensionComponent", () => { { provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } }, { provide: SYSTEM_THEME_OBSERVABLE, useValue: new BehaviorSubject("system") }, { provide: ThemeStateService, useValue: { selectedTheme$: new BehaviorSubject("system") } }, - { provide: AnonLayoutWrapperDataService, useValue: { setAnonLayoutWrapperData } }, { provide: AccountService, useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) }, @@ -133,14 +128,6 @@ describe("SetupExtensionComponent", () => { tick(); expect(component["state"]).toBe(SetupExtensionState.ManualOpen); - expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({ - pageTitle: { - key: "somethingWentWrong", - }, - pageIcon: BrowserExtensionIcon, - hideCardWrapper: false, - maxWidth: "md", - }); })); }); }); diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts index 974e73bc91e..809e404f5f1 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -5,7 +5,7 @@ import { Router, RouterModule } from "@angular/router"; import { firstValueFrom, pairwise, startWith } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BrowserExtensionIcon, Party } from "@bitwarden/assets/svg"; +import { Party } from "@bitwarden/assets/svg"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -14,7 +14,6 @@ import { StateProvider } from "@bitwarden/common/platform/state"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url"; import { - AnonLayoutWrapperDataService, ButtonComponent, CenterPositionStrategy, DialogRef, @@ -68,7 +67,6 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { private stateProvider = inject(StateProvider); private accountService = inject(AccountService); private document = inject(DOCUMENT); - private anonLayoutWrapperDataService = inject(AnonLayoutWrapperDataService); protected SetupExtensionState = SetupExtensionState; protected PartyIcon = Party; @@ -144,6 +142,16 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { } } + get showSuccessUI(): boolean { + const successStates = [ + SetupExtensionState.Success, + SetupExtensionState.AlreadyInstalled, + SetupExtensionState.ManualOpen, + ] as string[]; + + return successStates.includes(this.state); + } + /** Opens the add extension later dialog */ addItLater() { this.dialogRef = this.dialogService.open( @@ -161,16 +169,6 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { async openExtension() { await this.webBrowserExtensionInteractionService.openExtension().catch(() => { this.state = SetupExtensionState.ManualOpen; - - // Update the anon layout data to show the proper error design - this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ - pageTitle: { - key: "somethingWentWrong", - }, - pageIcon: BrowserExtensionIcon, - hideCardWrapper: false, - maxWidth: "md", - }); }); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index dccb6d28af5..90468c61d5c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -11351,14 +11351,6 @@ "openedExtensionViewAtRiskPasswords": { "message": "Successfully opened the Bitwarden browser extension. You can now review your at-risk passwords." }, - "openExtensionManuallyPart1": { - "message": "We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon", - "description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'" - }, - "openExtensionManuallyPart2": { - "message": "from the toolbar.", - "description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'" - }, "resellerRenewalWarningMsg": { "message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.", "placeholders": { @@ -11679,11 +11671,8 @@ "bitwardenExtensionInstalled": { "message": "Bitwarden extension installed!" }, - "openTheBitwardenExtension": { - "message": "Open the Bitwarden extension" - }, - "bitwardenExtensionInstalledOpenExtension": { - "message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling." + "bitwardenExtensionIsInstalled": { + "message": "Bitwarden extension is installed!" }, "openExtensionToAutofill": { "message": "Open the extension to log in and start autofilling." @@ -11699,6 +11688,14 @@ "message": "Learning Center", "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'" }, + "openExtensionFromToolbarPart1": { + "message": "If the extension didn't open, you may need to open Bitwarden from the icon ", + "description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'If the extension didn't open, you may need to open Bitwarden from the icon [Bitwarden Icon] on the toolbar.'" + }, + "openExtensionFromToolbarPart2": { + "message": " on the toolbar.", + "description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'If the extension didn't open, you may need to open Bitwarden from the icon [Bitwarden Icon] on the toolbar.'" + }, "gettingStartedWithBitwardenPart3": { "message": "Help Center", "description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'" From 23ac477bbc120c331da5cb70bea86c6b8c2cc91a Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 21 Nov 2025 16:01:41 -0500 Subject: [PATCH 04/44] chore(feature-flag): Removed pm-28325-remove-pm-22110-disable-alternate-login-methods flag --- .../organization-options.component.ts | 17 +----- .../auth/src/angular/login/login.component.ts | 11 +--- ...ault-login-success-handler.service.spec.ts | 58 +++++-------------- .../default-login-success-handler.service.ts | 21 +++---- libs/common/src/enums/feature-flag.enum.ts | 2 - 5 files changed, 27 insertions(+), 82 deletions(-) diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 981e5703cb3..3b707f2d78c 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -25,7 +25,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -181,13 +180,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { message: this.i18nService.t("unlinkedSso"), }); - const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM22110_DisableAlternateLoginMethods, - ); - - if (disableAlternateLoginMethodsFlagEnabled) { - await this.removeEmailFromSsoRequiredCacheIfPresent(); - } + await this.removeEmailFromSsoRequiredCacheIfPresent(); } catch (e) { this.logService.error(e); } @@ -214,13 +207,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { message: this.i18nService.t("leftOrganization"), }); - const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM22110_DisableAlternateLoginMethods, - ); - - if (disableAlternateLoginMethodsFlagEnabled) { - await this.removeEmailFromSsoRequiredCacheIfPresent(); - } + await this.removeEmailFromSsoRequiredCacheIfPresent(); } catch (e) { this.logService.error(e); } diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 51379ed213e..0b011b5641f 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -205,14 +205,9 @@ export class LoginComponent implements OnInit, OnDestroy { await this.loadRememberedEmail(); } - const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM22110_DisableAlternateLoginMethods, - ); - if (disableAlternateLoginMethodsFlagEnabled) { - // This SSO required check should come after email has had a chance to be pre-filled (if it - // was found in query params or was the remembered email) - await this.determineIfSsoRequired(); - } + // This SSO required check should come after email has had a chance to be pre-filled (if it + // was found in query params or was the remembered email) + await this.determineIfSsoRequired(); } private async desktopOnInit(): Promise { diff --git a/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.spec.ts b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.spec.ts index 86f7be8dfc7..975e065e21e 100644 --- a/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.spec.ts +++ b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.spec.ts @@ -1,7 +1,6 @@ import { MockProxy, mock } from "jest-mock-extended"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; @@ -58,62 +57,35 @@ describe("DefaultLoginSuccessHandlerService", () => { expect(loginEmailService.clearLoginEmail).toHaveBeenCalled(); }); - describe("when PM22110_DisableAlternateLoginMethods flag is disabled", () => { + it("should get SSO email", async () => { + await service.run(userId); + + expect(ssoLoginService.getSsoEmail).toHaveBeenCalled(); + }); + + describe("given SSO email is not found", () => { beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue(false); + ssoLoginService.getSsoEmail.mockResolvedValue(null); }); - it("should not check SSO requirements", async () => { + it("should log error and return early", async () => { await service.run(userId); - expect(ssoLoginService.getSsoEmail).not.toHaveBeenCalled(); + expect(logService.error).toHaveBeenCalledWith("SSO login email not found."); expect(ssoLoginService.updateSsoRequiredCache).not.toHaveBeenCalled(); }); }); - describe("given PM22110_DisableAlternateLoginMethods flag is enabled", () => { + describe("given SSO email is found", () => { beforeEach(() => { - configService.getFeatureFlag.mockResolvedValue(true); + ssoLoginService.getSsoEmail.mockResolvedValue(testEmail); }); - it("should check feature flag", async () => { + it("should call updateSsoRequiredCache() and clearSsoEmail()", async () => { await service.run(userId); - expect(configService.getFeatureFlag).toHaveBeenCalledWith( - FeatureFlag.PM22110_DisableAlternateLoginMethods, - ); - }); - - it("should get SSO email", async () => { - await service.run(userId); - - expect(ssoLoginService.getSsoEmail).toHaveBeenCalled(); - }); - - describe("given SSO email is not found", () => { - beforeEach(() => { - ssoLoginService.getSsoEmail.mockResolvedValue(null); - }); - - it("should log error and return early", async () => { - await service.run(userId); - - expect(logService.error).toHaveBeenCalledWith("SSO login email not found."); - expect(ssoLoginService.updateSsoRequiredCache).not.toHaveBeenCalled(); - }); - }); - - describe("given SSO email is found", () => { - beforeEach(() => { - ssoLoginService.getSsoEmail.mockResolvedValue(testEmail); - }); - - it("should call updateSsoRequiredCache() and clearSsoEmail()", async () => { - await service.run(userId); - - expect(ssoLoginService.updateSsoRequiredCache).toHaveBeenCalledWith(testEmail, userId); - expect(ssoLoginService.clearSsoEmail).toHaveBeenCalled(); - }); + expect(ssoLoginService.updateSsoRequiredCache).toHaveBeenCalledWith(testEmail, userId); + expect(ssoLoginService.clearSsoEmail).toHaveBeenCalled(); }); }); }); diff --git a/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts index 78003a4fca0..27d058c311a 100644 --- a/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts +++ b/libs/auth/src/common/services/login-success-handler/default-login-success-handler.service.ts @@ -1,5 +1,4 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { UserId } from "@bitwarden/common/types/guid"; @@ -23,20 +22,14 @@ export class DefaultLoginSuccessHandlerService implements LoginSuccessHandlerSer await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(userId); await this.loginEmailService.clearLoginEmail(); - const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag( - FeatureFlag.PM22110_DisableAlternateLoginMethods, - ); + const ssoLoginEmail = await this.ssoLoginService.getSsoEmail(); - if (disableAlternateLoginMethodsFlagEnabled) { - const ssoLoginEmail = await this.ssoLoginService.getSsoEmail(); - - if (!ssoLoginEmail) { - this.logService.error("SSO login email not found."); - return; - } - - await this.ssoLoginService.updateSsoRequiredCache(ssoLoginEmail, userId); - await this.ssoLoginService.clearSsoEmail(); + if (!ssoLoginEmail) { + this.logService.error("SSO login email not found."); + return; } + + await this.ssoLoginService.updateSsoRequiredCache(ssoLoginEmail, userId); + await this.ssoLoginService.clearSsoEmail(); } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index d06a14d242f..17d5f4e9df5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -16,7 +16,6 @@ export enum FeatureFlag { BlockClaimedDomainAccountCreation = "block-claimed-domain-account-creation", /* Auth */ - PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods", PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin", /* Autofill */ @@ -118,7 +117,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultLoadingSkeletons]: FALSE, /* Auth */ - [FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE, [FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE, /* Billing */ From 489eb4005780046a4f87719e5a30ae4449958fc2 Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:02:22 -0700 Subject: [PATCH 05/44] Desktop Autotype fix IPC error handling (#17332) * Desktop Autotype fix IPC error handling * TS lint * sweep sweep: fix unecessary member name qualifier --- .../src/autofill/main/main-desktop-autotype.service.ts | 9 +++++++++ apps/desktop/src/autofill/models/autotype-errors.ts | 8 ++++++++ apps/desktop/src/autofill/preload.ts | 10 ++++++---- 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/autofill/models/autotype-errors.ts diff --git a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts index e33ab0d4c3b..4dcf05a4220 100644 --- a/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts +++ b/apps/desktop/src/autofill/main/main-desktop-autotype.service.ts @@ -5,6 +5,7 @@ import { LogService } from "@bitwarden/logging"; import { WindowMain } from "../../main/window.main"; import { stringIsNotUndefinedNullAndEmpty } from "../../utils"; +import { AutotypeMatchError } from "../models/autotype-errors"; import { AutotypeVaultData } from "../models/autotype-vault-data"; import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut"; @@ -56,6 +57,14 @@ export class MainDesktopAutotypeService { this.doAutotype(vaultData, this.autotypeKeyboardShortcut.getArrayFormat()); } }); + + ipcMain.on("autofill.completeAutotypeError", (_event, matchError: AutotypeMatchError) => { + this.logService.debug( + "autofill.completeAutotypeError", + "No match for window: " + matchError.windowTitle, + ); + this.logService.error("autofill.completeAutotypeError", matchError.errorMessage); + }); } disableAutotype() { diff --git a/apps/desktop/src/autofill/models/autotype-errors.ts b/apps/desktop/src/autofill/models/autotype-errors.ts new file mode 100644 index 00000000000..9e59b102302 --- /dev/null +++ b/apps/desktop/src/autofill/models/autotype-errors.ts @@ -0,0 +1,8 @@ +/** + * This error is surfaced when there is no matching + * vault item found. + */ +export interface AutotypeMatchError { + windowTitle: string; + errorMessage: string; +} diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts index 22b5cdf9463..e839ac223b7 100644 --- a/apps/desktop/src/autofill/preload.ts +++ b/apps/desktop/src/autofill/preload.ts @@ -5,6 +5,7 @@ import type { autofill } from "@bitwarden/desktop-napi"; import { Command } from "../platform/main/autofill/command"; import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main"; +import { AutotypeMatchError } from "./models/autotype-errors"; import { AutotypeVaultData } from "./models/autotype-vault-data"; export default { @@ -141,7 +142,7 @@ export default { ipcRenderer.on( "autofill.listenAutotypeRequest", ( - event, + _event, data: { windowTitle: string; }, @@ -150,10 +151,11 @@ export default { fn(windowTitle, (error, vaultData) => { if (error) { - ipcRenderer.send("autofill.completeError", { + const matchError: AutotypeMatchError = { windowTitle, - error: error.message, - }); + errorMessage: error.message, + }; + ipcRenderer.send("autofill.completeAutotypeError", matchError); return; } if (vaultData !== null) { From 13940a74ae88e146286f40e9c6dec164d11c21db Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Sat, 22 Nov 2025 11:53:45 +0100 Subject: [PATCH 06/44] Fix biometrics unlock when pin is enabled (#17528) --- .../browser/src/background/main.background.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index f59b6648486..fecc47af981 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -726,17 +726,6 @@ export default class MainBackground { const pinStateService = new PinStateService(this.stateProvider); - this.pinService = new PinService( - this.accountService, - this.encryptService, - this.kdfConfigService, - this.keyGenerationService, - this.logService, - this.keyService, - this.sdkService, - pinStateService, - ); - this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); @@ -756,16 +745,6 @@ export default class MainBackground { VaultTimeoutStringType.OnRestart, // default vault timeout ); - this.biometricsService = new BackgroundBrowserBiometricsService( - runtimeNativeMessagingBackground, - this.logService, - this.keyService, - this.biometricStateService, - this.messagingService, - this.vaultTimeoutSettingsService, - this.pinService, - ); - this.apiService = new ApiService( this.tokenService, this.platformUtilsService, @@ -849,6 +828,27 @@ export default class MainBackground { this.configService, ); + this.pinService = new PinService( + this.accountService, + this.encryptService, + this.kdfConfigService, + this.keyGenerationService, + this.logService, + this.keyService, + this.sdkService, + pinStateService, + ); + + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + this.logService, + this.keyService, + this.biometricStateService, + this.messagingService, + this.vaultTimeoutSettingsService, + this.pinService, + ); + this.passwordStrengthService = new PasswordStrengthService(); this.passwordGenerationService = legacyPasswordGenerationServiceFactory( From 637f4961bb239b671895c972ea9f6f37c2d1a978 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:23:03 +0100 Subject: [PATCH 07/44] [deps] Billing: Update braintree-web-drop-in to v1.46.0 (#14451) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> --- package-lock.json | 112 +++++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 51 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index 005eb38d80b..9ec580e3a5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "@nx/js": "21.6.8", "@nx/webpack": "21.6.8", "big-integer": "1.6.52", - "braintree-web-drop-in": "1.44.0", + "braintree-web-drop-in": "1.46.0", "buffer": "6.0.3", "bufferutil": "4.0.9", "chalk": "4.1.2", @@ -4798,15 +4798,15 @@ "link": true }, "node_modules/@braintree/asset-loader": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@braintree/asset-loader/-/asset-loader-2.0.1.tgz", - "integrity": "sha512-OGAoBA5MRVsr5qg0sXM6NMJbqHnYZhBudtM6WGgpQnoX42fjUYbE6Y6qFuuerD5z3lsOAjnu80DooBs1VBuh5Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@braintree/asset-loader/-/asset-loader-2.0.3.tgz", + "integrity": "sha512-uREap1j30wKRlC0mK99nNPMpEp77NtB6XixpDfFJPZHmkrmw7IB4skKe+26LZBK1H6oSainFhAyKoP7x3eyOKA==", "license": "MIT" }, "node_modules/@braintree/browser-detection": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@braintree/browser-detection/-/browser-detection-2.0.1.tgz", - "integrity": "sha512-wpRI7AXEUh6o3ILrJbpNOYE7ItfjX/S8JZP7Z5FF66ULngBGYOqE8SeLlLKXG69Nc07HtlL/6nk/h539iz9hcQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@braintree/browser-detection/-/browser-detection-2.0.2.tgz", + "integrity": "sha512-Zrv/pyodvwv/hsjsBKXKVcwHZOkx4A/5Cy2hViXtqghAhLd3483bYUIfHZJE5JKTrd018ny1FI5pN1PHFtW7vw==", "license": "MIT" }, "node_modules/@braintree/event-emitter": { @@ -4822,9 +4822,9 @@ "license": "MIT" }, "node_modules/@braintree/iframer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@braintree/iframer/-/iframer-2.0.0.tgz", - "integrity": "sha512-x1kHOyIJNDvi4P1s6pVBZhqhBa1hqDG9+yzcsCR1oNVC0LxH9CAP8bKxioT8/auY1sUyy+D8T4Vp/jv7QqSqLQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@braintree/iframer/-/iframer-2.0.1.tgz", + "integrity": "sha512-t1zJX5+f1yxHAzBJPaQT/XVMocKodUqjTE+hYvuxxWjqEZIbH8/eT5b5n767jY16mYw3+XiDkKHqcp4Pclq1wg==", "license": "MIT" }, "node_modules/@braintree/sanitize-url": { @@ -4834,9 +4834,9 @@ "license": "MIT" }, "node_modules/@braintree/uuid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@braintree/uuid/-/uuid-1.0.0.tgz", - "integrity": "sha512-AtI5hfttWSuWAgcwLUZdcZ7Fp/8jCCUf9JTs7+Xow9ditU28zuoBovqq083yph2m3SxPYb84lGjOq+cXlXBvJg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@braintree/uuid/-/uuid-1.0.1.tgz", + "integrity": "sha512-Tgu5GoODkf4oj4aLlVIapEPEfjitIHrg5ftqY6pa5Ghr4ZUA9XtZIIZ6ZPdP9x8/X0lt/FB8tRq83QuCQCwOrA==", "license": "ISC" }, "node_modules/@braintree/wrap-promise": { @@ -17762,40 +17762,40 @@ } }, "node_modules/braintree-web": { - "version": "3.113.0", - "resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.113.0.tgz", - "integrity": "sha512-qykYxZyld4X1tRNgXZQ3ZGzmhDGTBTRQ6Q24KaG9PuYqo+P2TVDEDOVC6tRbkx2RUIdXLv2M6WpkG7oLqEia9Q==", + "version": "3.123.2", + "resolved": "https://registry.npmjs.org/braintree-web/-/braintree-web-3.123.2.tgz", + "integrity": "sha512-N4IH75vKY67eONc0Ao4e7F+XagFW+3ok+Nfs/eOjw5D/TUt03diMAQ8woOwJghi2ql6/yjqNzZi2zE/sTWXmJg==", "license": "MIT", "dependencies": { - "@braintree/asset-loader": "2.0.1", - "@braintree/browser-detection": "2.0.1", + "@braintree/asset-loader": "2.0.3", + "@braintree/browser-detection": "2.0.2", "@braintree/event-emitter": "0.4.1", "@braintree/extended-promise": "1.0.0", - "@braintree/iframer": "2.0.0", + "@braintree/iframer": "2.0.1", "@braintree/sanitize-url": "7.0.4", - "@braintree/uuid": "1.0.0", + "@braintree/uuid": "1.0.1", "@braintree/wrap-promise": "2.1.0", "@paypal/accelerated-checkout-loader": "1.1.0", - "card-validator": "10.0.0", - "credit-card-type": "10.0.1", - "framebus": "6.0.0", - "inject-stylesheet": "6.0.1", + "card-validator": "10.0.3", + "credit-card-type": "10.0.2", + "framebus": "6.0.3", + "inject-stylesheet": "6.0.2", "promise-polyfill": "8.2.3", - "restricted-input": "3.0.5" + "restricted-input": "4.0.3" } }, "node_modules/braintree-web-drop-in": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.44.0.tgz", - "integrity": "sha512-maOq9SwiXztIzixJhOras7K44x4UIqqnkyQMYAJqxQ8WkADv9AkflCu2j3IeVYCus/Th9gWWFHcBugn3C4sZGw==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/braintree-web-drop-in/-/braintree-web-drop-in-1.46.0.tgz", + "integrity": "sha512-KxCjJpaigoMajYD/iIA+ohXaI6Olt2Bj/Yu45WpJOjolKO9n1UmXl9bsq9UIiGOFIGqi/JWva1wI4cIHHvcI1A==", "license": "MIT", "dependencies": { - "@braintree/asset-loader": "2.0.1", - "@braintree/browser-detection": "2.0.1", + "@braintree/asset-loader": "2.0.3", + "@braintree/browser-detection": "2.0.2", "@braintree/event-emitter": "0.4.1", - "@braintree/uuid": "1.0.0", + "@braintree/uuid": "1.0.1", "@braintree/wrap-promise": "2.1.0", - "braintree-web": "3.113.0" + "braintree-web": "3.123.2" } }, "node_modules/browser-assert": { @@ -18444,20 +18444,14 @@ "license": "CC-BY-4.0" }, "node_modules/card-validator": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.0.tgz", - "integrity": "sha512-2fLyCBOxO7/b56sxoYav8FeJqv9bWpZSyKq8sXKxnpxTGXHnM/0c8WEKG+ZJ+OXFcabnl98pD0EKBtTn+Tql0g==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.3.tgz", + "integrity": "sha512-xOEDsK3hojV0OIpmrR64eZGpngnOqRDEP20O+WSRtvjLSW6nyekW4i2N9SzYg679uFO3RyHcFHxb+mml5tXc4A==", "license": "MIT", "dependencies": { - "credit-card-type": "^9.1.0" + "credit-card-type": "^10.0.2" } }, - "node_modules/card-validator/node_modules/credit-card-type": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-9.1.0.tgz", - "integrity": "sha512-CpNFuLxiPFxuZqhSKml3M+t0K/484pMAnfYWH14JoD7OZMnmC0Lmo+P7JX9SobqFpRoo7ifA18kOHdxJywYPEA==", - "license": "MIT" - }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", @@ -19692,9 +19686,9 @@ "license": "MIT" }, "node_modules/credit-card-type": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.0.1.tgz", - "integrity": "sha512-vQOuWmBgsgG1ovGeDi8m6Zeu1JaqH/JncrxKmaqMbv/LunyOQdLiQhPHtOsNlbUI05TocR5nod/Mbs3HYtr6sQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.0.2.tgz", + "integrity": "sha512-vt/iQokU0mtrT7ceRU75FSmWnIh5JFpLsUUUWYRmztYekOGm0ZbCuzwFTbNkq41k92y+0B8ChscFhRN9DhVZEA==", "license": "MIT" }, "node_modules/cross-dirname": { @@ -23410,20 +23404,14 @@ } }, "node_modules/framebus": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/framebus/-/framebus-6.0.0.tgz", - "integrity": "sha512-bL9V68hVaVBCY9rveoWbPFFI9hAXIJtESs51B+9XmzvMt38+wP8b4VdiJsavjMS6NfPZ/afQ/jc2qaHmSGI1kQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/framebus/-/framebus-6.0.3.tgz", + "integrity": "sha512-G/N2p+kFZ1xPBge7tbtTq2KcTR1kSKs1rVbTqH//WdtvJSexS33fsTTOq3yfUWvUczqhujyaFc+omawC9YyRBg==", "license": "MIT", "dependencies": { - "@braintree/uuid": "^0.1.0" + "@braintree/uuid": "^1.0.0" } }, - "node_modules/framebus/node_modules/@braintree/uuid": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@braintree/uuid/-/uuid-0.1.0.tgz", - "integrity": "sha512-YvZJdlNcK5EnR+7M8AjgEAf4Qx696+FOSYlPfy5ePn80vODtVAUU0FxHnzKZC0og1VbDNQDDiwhthR65D4Na0g==", - "license": "ISC" - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -24987,9 +24975,9 @@ } }, "node_modules/inject-stylesheet": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/inject-stylesheet/-/inject-stylesheet-6.0.1.tgz", - "integrity": "sha512-2fvune1D4+8mvJoLVo95ncY4HrDkIaYIReRzXv8tkWFgdG9iuc5QuX57gtSDPWTWQI/f5BGwwtH85wxHouzucg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inject-stylesheet/-/inject-stylesheet-6.0.2.tgz", + "integrity": "sha512-sswMueya1LXEfwcy7KXPuq3zAW6HvgAeViApEhIaCviCkP4XYoKrQj8ftEmxPmIHn88X4R3xOAsnN/QCPvVKWw==", "license": "MIT" }, "node_modules/inquirer": { @@ -36154,12 +36142,12 @@ "license": "ISC" }, "node_modules/restricted-input": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/restricted-input/-/restricted-input-3.0.5.tgz", - "integrity": "sha512-lUuXZ3wUnHURRarj5/0C8vomWIfWJO+p7T6RYwB46v7Oyuyr3yyupU+i7SjqUv4S6RAeAAZt1C/QCLJ9xhQBow==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/restricted-input/-/restricted-input-4.0.3.tgz", + "integrity": "sha512-VpkwT5Fr3DhvoRZfPnmHDhnYAYETjjNzDlvA4NlW0iknFS47C5X4OCHEpOOxaPjvmka5V8d1ty1jVVoorZKvHg==", "license": "MIT", "dependencies": { - "@braintree/browser-detection": "^1.12.1" + "@braintree/browser-detection": "^1.17.2" } }, "node_modules/restricted-input/node_modules/@braintree/browser-detection": { diff --git a/package.json b/package.json index 7ca866b3c4d..87a78a30796 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,7 @@ "@nx/js": "21.6.8", "@nx/webpack": "21.6.8", "big-integer": "1.6.52", - "braintree-web-drop-in": "1.44.0", + "braintree-web-drop-in": "1.46.0", "buffer": "6.0.3", "bufferutil": "4.0.9", "chalk": "4.1.2", From 7e32d0a59faa97a897cc3c47e98403396425d360 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 24 Nov 2025 16:36:23 +0100 Subject: [PATCH 08/44] [PM-27564] Self-host configuration is not applied with nx build (#17279) * fix: web not using env variables * fix: apply claude suggestion * fix: remove non-working serve targets --- apps/web/project.json | 30 --------------------- apps/web/webpack.base.js | 14 +++++++--- apps/web/webpack.config.js | 1 + bitwarden_license/bit-web/webpack.config.js | 2 ++ 4 files changed, 13 insertions(+), 34 deletions(-) diff --git a/apps/web/project.json b/apps/web/project.json index 4f51bf22740..710fd7cb5e7 100644 --- a/apps/web/project.json +++ b/apps/web/project.json @@ -154,45 +154,15 @@ }, "configurations": { "oss": { - "buildTarget": "web:build:oss" - }, - "oss-dev": { "buildTarget": "web:build:oss-dev" }, "commercial": { - "buildTarget": "web:build:commercial" - }, - "commercial-dev": { "buildTarget": "web:build:commercial-dev" }, - "commercial-qa": { - "buildTarget": "web:build:commercial-qa" - }, - "commercial-cloud": { - "buildTarget": "web:build:commercial-cloud" - }, - "commercial-euprd": { - "buildTarget": "web:build:commercial-euprd" - }, - "commercial-euqa": { - "buildTarget": "web:build:commercial-euqa" - }, - "commercial-usdev": { - "buildTarget": "web:build:commercial-usdev" - }, - "commercial-ee": { - "buildTarget": "web:build:commercial-ee" - }, "oss-selfhost": { - "buildTarget": "web:build:oss-selfhost" - }, - "oss-selfhost-dev": { "buildTarget": "web:build:oss-selfhost-dev" }, "commercial-selfhost": { - "buildTarget": "web:build:commercial-selfhost" - }, - "commercial-selfhost-dev": { "buildTarget": "web:build:commercial-selfhost-dev" } } diff --git a/apps/web/webpack.base.js b/apps/web/webpack.base.js index f1e627a58a8..cc17b3b7cfd 100644 --- a/apps/web/webpack.base.js +++ b/apps/web/webpack.base.js @@ -13,9 +13,11 @@ const config = require(path.resolve(__dirname, "config.js")); const pjson = require(path.resolve(__dirname, "package.json")); module.exports.getEnv = function getEnv(params) { - const ENV = params.env || (process.env.ENV == null ? "development" : process.env.ENV); - const NODE_ENV = process.env.NODE_ENV == null ? "development" : process.env.NODE_ENV; - const LOGGING = process.env.LOGGING != "false"; + const ENV = params.env?.ENV ?? process.env?.ENV ?? "development"; + const NODE_ENV = params.env?.NODE_ENV ?? process.env?.NODE_ENV ?? "development"; + const LOGGING = + params.env?.LOGGING ?? + (process.env?.LOGGING === undefined ? true : process.env.LOGGING !== "false"); return { ENV, NODE_ENV, LOGGING }; }; @@ -35,7 +37,11 @@ const DEFAULT_PARAMS = { * tsConfig: string; * outputPath?: string; * mode?: string; - * env?: string; + * env?: { + * ENV?: string; + * NODE_ENV?: string; + * LOGGING?: boolean; + * }; * importAliases?: import("webpack").ResolveOptions["alias"]; * }} params */ diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 962d72ac825..275a6a5f3b0 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -15,6 +15,7 @@ module.exports = (webpackConfig, context) => { }, tsConfig: "apps/web/tsconfig.build.json", outputPath: path.resolve(context.context.root, context.options.outputPath), + env: context.options.env, }); } else { return buildConfig({ diff --git a/bitwarden_license/bit-web/webpack.config.js b/bitwarden_license/bit-web/webpack.config.js index 6433eee59f6..8ab719072f6 100644 --- a/bitwarden_license/bit-web/webpack.config.js +++ b/bitwarden_license/bit-web/webpack.config.js @@ -3,6 +3,7 @@ const { buildConfig } = require(path.resolve(__dirname, "../../apps/web/webpack. module.exports = (webpackConfig, context) => { const isNxBuild = context && context.options; + if (isNxBuild) { return buildConfig({ configName: "Commercial", @@ -23,6 +24,7 @@ module.exports = (webpackConfig, context) => { alias: "@bitwarden/commercial-sdk-internal", }, ], + env: context.options.env, }); } else { return buildConfig({ From 3a4eec38a1b1115601b350e0a3387e7448e650cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 15:38:40 +0000 Subject: [PATCH 09/44] [deps] Platform: Update Rust crate arboard to v3.6.1 (#17547) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- apps/desktop/desktop_native/Cargo.lock | 5 +++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 475253f935f..a1cdb9f26eb 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -120,9 +120,9 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "arboard" -version = "3.6.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "log", @@ -131,6 +131,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", + "windows-sys 0.60.2", "wl-clipboard-rs", "x11rb", ] diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 864b743962d..0b09daa9bdd 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -22,7 +22,7 @@ publish = false aes = "=0.8.4" aes-gcm = "=0.10.3" anyhow = "=1.0.94" -arboard = { version = "=3.6.0", default-features = false } +arboard = { version = "=3.6.1", default-features = false } ashpd = "=0.11.0" base64 = "=0.22.1" bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" } From 5779df241721634ffbf78a4229272bab4eb15e01 Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:46:28 -0600 Subject: [PATCH 10/44] Correct phishing blocker file structure (#17477) --- .../{pages => popup}/phishing-warning.component.html | 0 .../{pages => popup}/phishing-warning.component.ts | 3 --- .../{pages => popup}/phishing-warning.stories.ts | 2 -- .../{pages => popup}/protected-by-component.html | 0 .../{pages => popup}/protected-by-component.ts | 2 -- apps/browser/src/popup/app-routing.module.ts | 4 ++-- 6 files changed, 2 insertions(+), 9 deletions(-) rename apps/browser/src/dirt/phishing-detection/{pages => popup}/phishing-warning.component.html (100%) rename apps/browser/src/dirt/phishing-detection/{pages => popup}/phishing-warning.component.ts (93%) rename apps/browser/src/dirt/phishing-detection/{pages => popup}/phishing-warning.stories.ts (97%) rename apps/browser/src/dirt/phishing-detection/{pages => popup}/protected-by-component.html (100%) rename apps/browser/src/dirt/phishing-detection/{pages => popup}/protected-by-component.ts (86%) diff --git a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.html b/apps/browser/src/dirt/phishing-detection/popup/phishing-warning.component.html similarity index 100% rename from apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.html rename to apps/browser/src/dirt/phishing-detection/popup/phishing-warning.component.html diff --git a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts b/apps/browser/src/dirt/phishing-detection/popup/phishing-warning.component.ts similarity index 93% rename from apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts rename to apps/browser/src/dirt/phishing-detection/popup/phishing-warning.component.ts index 589b880b206..d8e9895237c 100644 --- a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.component.ts +++ b/apps/browser/src/dirt/phishing-detection/popup/phishing-warning.component.ts @@ -1,8 +1,5 @@ -// eslint-disable-next-line no-restricted-imports import { CommonModule } from "@angular/common"; -// eslint-disable-next-line no-restricted-imports import { Component, inject } from "@angular/core"; -// eslint-disable-next-line no-restricted-imports import { ActivatedRoute, RouterModule } from "@angular/router"; import { firstValueFrom, map } from "rxjs"; diff --git a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.stories.ts b/apps/browser/src/dirt/phishing-detection/popup/phishing-warning.stories.ts similarity index 97% rename from apps/browser/src/dirt/phishing-detection/pages/phishing-warning.stories.ts rename to apps/browser/src/dirt/phishing-detection/popup/phishing-warning.stories.ts index e79543605c2..32b3c102c36 100644 --- a/apps/browser/src/dirt/phishing-detection/pages/phishing-warning.stories.ts +++ b/apps/browser/src/dirt/phishing-detection/popup/phishing-warning.stories.ts @@ -1,5 +1,3 @@ -// TODO: This needs to be dealt with by moving this folder or updating the lint rule. -/* eslint-disable no-restricted-imports */ import { ActivatedRoute, RouterModule } from "@angular/router"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { BehaviorSubject, of } from "rxjs"; diff --git a/apps/browser/src/dirt/phishing-detection/pages/protected-by-component.html b/apps/browser/src/dirt/phishing-detection/popup/protected-by-component.html similarity index 100% rename from apps/browser/src/dirt/phishing-detection/pages/protected-by-component.html rename to apps/browser/src/dirt/phishing-detection/popup/protected-by-component.html diff --git a/apps/browser/src/dirt/phishing-detection/pages/protected-by-component.ts b/apps/browser/src/dirt/phishing-detection/popup/protected-by-component.ts similarity index 86% rename from apps/browser/src/dirt/phishing-detection/pages/protected-by-component.ts rename to apps/browser/src/dirt/phishing-detection/popup/protected-by-component.ts index 71cdac89aa2..8da916af5e6 100644 --- a/apps/browser/src/dirt/phishing-detection/pages/protected-by-component.ts +++ b/apps/browser/src/dirt/phishing-detection/popup/protected-by-component.ts @@ -1,6 +1,4 @@ -// eslint-disable-next-line no-restricted-imports import { CommonModule } from "@angular/common"; -// eslint-disable-next-line no-restricted-imports import { Component } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 1834beb391e..a36396afa1a 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -56,8 +56,8 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; -import { PhishingWarning } from "../dirt/phishing-detection/pages/phishing-warning.component"; -import { ProtectedByComponent } from "../dirt/phishing-detection/pages/protected-by-component"; +import { PhishingWarning } from "../dirt/phishing-detection/popup/phishing-warning.component"; +import { ProtectedByComponent } from "../dirt/phishing-detection/popup/protected-by-component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import BrowserPopupUtils from "../platform/browser/browser-popup-utils"; import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service"; From 4c36a46ef27e0d5d3c91595d47ebdcad965a80b6 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 24 Nov 2025 18:03:16 +0100 Subject: [PATCH 11/44] Enable directive-class-suffix (#17385) --- .../navigation-switcher/navigation-switcher.stories.ts | 4 ++++ .../layouts/product-switcher/product-switcher.stories.ts | 4 ++++ eslint.config.mjs | 2 +- .../src/auth/components/user-verification.component.ts | 2 ++ .../src/directives/cipherListVirtualScroll.directive.ts | 2 ++ libs/components/src/form-control/form-control.module.ts | 6 +++--- .../form-control/{hint.component.ts => hint.directive.ts} | 2 +- libs/components/src/form-field/form-field.component.ts | 4 ++-- libs/components/src/switch/switch.component.ts | 4 ++-- libs/components/src/table/table-scroll.component.ts | 4 ++-- libs/components/src/table/table.module.ts | 6 +++--- libs/vault/src/directives/readonly-textarea.directive.ts | 2 ++ 12 files changed, 28 insertions(+), 14 deletions(-) rename libs/components/src/form-control/{hint.component.ts => hint.directive.ts} (90%) diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts index faf1b796b00..88132e56384 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.stories.ts @@ -30,6 +30,8 @@ import { NavigationProductSwitcherComponent } from "./navigation-switcher.compon selector: "[mockOrgs]", standalone: false, }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); @@ -49,6 +51,8 @@ class MockOrganizationService implements Partial { selector: "[mockProviders]", standalone: false, }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix class MockProviderService implements Partial { private static _providers = new BehaviorSubject([]); diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts index 4c6af713464..4581f5981e6 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.stories.ts @@ -30,6 +30,8 @@ import { ProductSwitcherService } from "./shared/product-switcher.service"; selector: "[mockOrgs]", standalone: false, }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix class MockOrganizationService implements Partial { private static _orgs = new BehaviorSubject([]); @@ -49,6 +51,8 @@ class MockOrganizationService implements Partial { selector: "[mockProviders]", standalone: false, }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix class MockProviderService implements Partial { private static _providers = new BehaviorSubject([]); diff --git a/eslint.config.mjs b/eslint.config.mjs index 7f9bb2284f7..1e12e0e1e19 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -63,7 +63,7 @@ export default tseslint.config( // TODO: Enable these. "@angular-eslint/component-class-suffix": "error", "@angular-eslint/contextual-lifecycle": "error", - "@angular-eslint/directive-class-suffix": 0, + "@angular-eslint/directive-class-suffix": "error", "@angular-eslint/no-empty-lifecycle-method": 0, "@angular-eslint/no-input-rename": 0, "@angular-eslint/no-inputs-metadata-property": "error", diff --git a/libs/angular/src/auth/components/user-verification.component.ts b/libs/angular/src/auth/components/user-verification.component.ts index 1f0659a92ff..a2cee2f1099 100644 --- a/libs/angular/src/auth/components/user-verification.component.ts +++ b/libs/angular/src/auth/components/user-verification.component.ts @@ -24,6 +24,8 @@ import { KeyService } from "@bitwarden/key-management"; selector: "app-user-verification", standalone: false, }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy { private _invalidSecret = false; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals diff --git a/libs/angular/src/directives/cipherListVirtualScroll.directive.ts b/libs/angular/src/directives/cipherListVirtualScroll.directive.ts index 442e01c1c79..8e7b5cc204d 100644 --- a/libs/angular/src/directives/cipherListVirtualScroll.directive.ts +++ b/libs/angular/src/directives/cipherListVirtualScroll.directive.ts @@ -45,6 +45,8 @@ export function _cipherListVirtualScrollStrategyFactory(cipherListDir: CipherLis }, ], }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export class CipherListVirtualScroll extends CdkFixedSizeVirtualScroll { _scrollStrategy: CipherListVirtualScrollStrategy; diff --git a/libs/components/src/form-control/form-control.module.ts b/libs/components/src/form-control/form-control.module.ts index 2646f36ecd3..d87284adbcd 100644 --- a/libs/components/src/form-control/form-control.module.ts +++ b/libs/components/src/form-control/form-control.module.ts @@ -1,11 +1,11 @@ import { NgModule } from "@angular/core"; import { FormControlComponent } from "./form-control.component"; -import { BitHintComponent } from "./hint.component"; +import { BitHintDirective } from "./hint.directive"; import { BitLabelComponent } from "./label.component"; @NgModule({ - imports: [BitLabelComponent, FormControlComponent, BitHintComponent], - exports: [FormControlComponent, BitLabelComponent, BitHintComponent], + imports: [BitLabelComponent, FormControlComponent, BitHintDirective], + exports: [FormControlComponent, BitLabelComponent, BitHintDirective], }) export class FormControlModule {} diff --git a/libs/components/src/form-control/hint.component.ts b/libs/components/src/form-control/hint.directive.ts similarity index 90% rename from libs/components/src/form-control/hint.component.ts rename to libs/components/src/form-control/hint.directive.ts index c1f21bf2545..110aefc30e7 100644 --- a/libs/components/src/form-control/hint.component.ts +++ b/libs/components/src/form-control/hint.directive.ts @@ -9,6 +9,6 @@ let nextId = 0; class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1 tw-text-xs", }, }) -export class BitHintComponent { +export class BitHintDirective { @HostBinding() id = `bit-hint-${nextId++}`; } diff --git a/libs/components/src/form-field/form-field.component.ts b/libs/components/src/form-field/form-field.component.ts index 3d49a58b1fc..10cf33b8257 100644 --- a/libs/components/src/form-field/form-field.component.ts +++ b/libs/components/src/form-field/form-field.component.ts @@ -15,7 +15,7 @@ import { import { I18nPipe } from "@bitwarden/ui-common"; -import { BitHintComponent } from "../form-control/hint.component"; +import { BitHintDirective } from "../form-control/hint.directive"; import { BitLabelComponent } from "../form-control/label.component"; import { inputBorderClasses } from "../input/input.directive"; @@ -31,7 +31,7 @@ import { BitFormFieldControl } from "./form-field-control"; }) export class BitFormFieldComponent implements AfterContentChecked { readonly input = contentChild.required(BitFormFieldControl); - readonly hint = contentChild(BitHintComponent); + readonly hint = contentChild(BitHintDirective); readonly label = contentChild(BitLabelComponent); readonly prefixContainer = viewChild>("prefixContainer"); diff --git a/libs/components/src/switch/switch.component.ts b/libs/components/src/switch/switch.component.ts index 30e1ac59d48..a93e274e8bb 100644 --- a/libs/components/src/switch/switch.component.ts +++ b/libs/components/src/switch/switch.component.ts @@ -13,7 +13,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; import { AriaDisableDirective } from "../a11y"; import { FormControlModule } from "../form-control/form-control.module"; -import { BitHintComponent } from "../form-control/hint.component"; +import { BitHintDirective } from "../form-control/hint.directive"; import { BitLabelComponent } from "../form-control/label.component"; let nextId = 0; @@ -56,7 +56,7 @@ export class SwitchComponent implements ControlValueAccessor, AfterViewInit { protected readonly disabled = model(false); protected readonly disabledReasonText = input(null); - private readonly hintComponent = contentChild(BitHintComponent); + private readonly hintComponent = contentChild(BitHintDirective); protected readonly disabledReasonTextId = `bit-switch-disabled-text-${nextId++}`; diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts index fcdd401a1a2..1ccb49a85e5 100644 --- a/libs/components/src/table/table-scroll.component.ts +++ b/libs/components/src/table/table-scroll.component.ts @@ -35,7 +35,7 @@ import { TableComponent } from "./table.component"; @Directive({ selector: "[bitRowDef]", }) -export class BitRowDef { +export class BitRowDefDirective { constructor(public template: TemplateRef) {} } @@ -69,7 +69,7 @@ export class TableScrollComponent /** Optional trackBy function. */ readonly trackBy = input | undefined>(); - protected readonly rowDef = contentChild(BitRowDef); + protected readonly rowDef = contentChild(BitRowDefDirective); /** * Height of the thead element (in pixels). diff --git a/libs/components/src/table/table.module.ts b/libs/components/src/table/table.module.ts index 68993612772..5e44f604481 100644 --- a/libs/components/src/table/table.module.ts +++ b/libs/components/src/table/table.module.ts @@ -5,14 +5,14 @@ import { NgModule } from "@angular/core"; import { CellDirective } from "./cell.directive"; import { RowDirective } from "./row.directive"; import { SortableComponent } from "./sortable.component"; -import { BitRowDef, TableScrollComponent } from "./table-scroll.component"; +import { BitRowDefDirective, TableScrollComponent } from "./table-scroll.component"; import { TableBodyDirective, TableComponent } from "./table.component"; @NgModule({ imports: [ CommonModule, ScrollingModule, - BitRowDef, + BitRowDefDirective, CellDirective, RowDirective, SortableComponent, @@ -21,7 +21,7 @@ import { TableBodyDirective, TableComponent } from "./table.component"; TableScrollComponent, ], exports: [ - BitRowDef, + BitRowDefDirective, CellDirective, RowDirective, SortableComponent, diff --git a/libs/vault/src/directives/readonly-textarea.directive.ts b/libs/vault/src/directives/readonly-textarea.directive.ts index 65bd9d6e353..17c5f865fb3 100644 --- a/libs/vault/src/directives/readonly-textarea.directive.ts +++ b/libs/vault/src/directives/readonly-textarea.directive.ts @@ -8,6 +8,8 @@ import { firstValueFrom } from "rxjs"; providers: [TextFieldModule], hostDirectives: [CdkTextareaAutosize], }) +// FIXME(https://bitwarden.atlassian.net/browse/PM-28232): Use Directive suffix +// eslint-disable-next-line @angular-eslint/directive-class-suffix export class VaultAutosizeReadOnlyTextArea implements AfterViewInit { constructor( @Host() private autosize: CdkTextareaAutosize, From 613e0c546143ef9d091380acb69cab94627c5627 Mon Sep 17 00:00:00 2001 From: Bryan Cunningham Date: Mon, 24 Nov 2025 13:08:25 -0500 Subject: [PATCH 12/44] [CL-925] add filled danger button (#17633) * add dangerPrimary button variant * add dangerPrimary to small story --- libs/components/src/button/button.component.ts | 8 ++++++++ libs/components/src/button/button.stories.ts | 8 ++++++++ libs/components/src/shared/button-like.abstraction.ts | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 0e50ccbe87a..7cae8fe974d 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -54,6 +54,14 @@ const buttonStyles: Record = { "hover:!tw-text-contrast", ...focusRing, ], + dangerPrimary: [ + "tw-border-danger-600", + "tw-bg-danger-600", + "!tw-text-contrast", + "hover:tw-bg-danger-700", + "hover:tw-border-danger-700", + ...focusRing, + ], unstyled: [], }; diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index 7319b47bce5..29c4dea3088 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -62,6 +62,13 @@ export const Primary: Story = { }, }; +export const DangerPrimary: Story = { + ...Default, + args: { + buttonType: "dangerPrimary", + }, +}; + export const Danger: Story = { ...Default, args: { @@ -77,6 +84,7 @@ export const Small: Story = { + `, }), diff --git a/libs/components/src/shared/button-like.abstraction.ts b/libs/components/src/shared/button-like.abstraction.ts index 63391743837..45a661b6ecb 100644 --- a/libs/components/src/shared/button-like.abstraction.ts +++ b/libs/components/src/shared/button-like.abstraction.ts @@ -1,6 +1,6 @@ import { ModelSignal } from "@angular/core"; -export type ButtonType = "primary" | "secondary" | "danger" | "unstyled"; +export type ButtonType = "primary" | "secondary" | "danger" | "dangerPrimary" | "unstyled"; export type ButtonSize = "default" | "small"; From 883ff8968e366edeac319f1531d97a30c2c2b6cc Mon Sep 17 00:00:00 2001 From: blackwood Date: Mon, 24 Nov 2025 14:08:11 -0500 Subject: [PATCH 13/44] Allows limited internal message posting when host experience content is controlled (#17313) --- .../src/background/runtime.background.ts | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 798a7583f85..597babdc777 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -293,14 +293,24 @@ export default class RuntimeBackground { case "openPopup": await this.openPopup(); break; - case VaultMessages.OpenAtRiskPasswords: + case VaultMessages.OpenAtRiskPasswords: { + if (await this.shouldRejectManyOriginMessage(msg)) { + return; + } + await this.main.openAtRisksPasswordsPage(); this.announcePopupOpen(); break; - case VaultMessages.OpenBrowserExtensionToUrl: + } + case VaultMessages.OpenBrowserExtensionToUrl: { + if (await this.shouldRejectManyOriginMessage(msg)) { + return; + } + await this.main.openTheExtensionToPage(msg.url); this.announcePopupOpen(); break; + } case "bgUpdateContextMenu": case "editedCipher": case "addedCipher": @@ -312,10 +322,7 @@ export default class RuntimeBackground { break; } case "authResult": { - const env = await firstValueFrom(this.environmentService.environment$); - const vaultUrl = env.getWebVaultUrl(); - - if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) { + if (!(await this.isValidVaultReferrer(msg.referrer))) { return; } @@ -334,10 +341,7 @@ export default class RuntimeBackground { break; } case "webAuthnResult": { - const env = await firstValueFrom(this.environmentService.environment$); - const vaultUrl = env.getWebVaultUrl(); - - if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) { + if (!(await this.isValidVaultReferrer(msg.referrer))) { return; } @@ -372,6 +376,48 @@ export default class RuntimeBackground { } } + /** + * For messages that can originate from a vault host page or extension, validate referrer or external + * + * @param message + * @returns true if message fails validation + */ + private async shouldRejectManyOriginMessage(message: { + webExtSender: chrome.runtime.MessageSender; + }): Promise { + const isValidVaultReferrer = await this.isValidVaultReferrer( + Utils.getHostname(message?.webExtSender?.origin), + ); + + if (isValidVaultReferrer) { + return false; + } + + return isExternalMessage(message); + } + + /** + * Validates a message's referrer matches the configured web vault hostname. + * + * @param referrer - hostname from message source + * @returns true if referrer matches web vault + */ + private async isValidVaultReferrer(referrer: string | null | undefined): Promise { + if (!referrer) { + return false; + } + + const env = await firstValueFrom(this.environmentService.environment$); + const vaultUrl = env.getWebVaultUrl(); + const vaultHostname = Utils.getHostname(vaultUrl); + + if (!vaultHostname) { + return false; + } + + return vaultHostname === referrer; + } + private async autofillPage(tabToAutoFill: chrome.tabs.Tab) { const totpCode = await this.autofillService.doAutoFill({ tab: tabToAutoFill, From 43fd99b002eefe3571dcc9e86e97b3a4fd264448 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:49:05 -0800 Subject: [PATCH 14/44] [PM-24722][PM-27695] - add persistent callout in settings for non-premium users (#17246) * add persistent callout in settings for non-premium users * remove premium v2 component * add spec * remove premium-v2.component.html * fix title * fix typo * conditionally render h2 * re-add pemiumv2component. change class prop to observable * change from bold to semibold * remove unecessary tw classes. use transform: booleanAttribute * add spotlight specs * code cleanup --- apps/browser/src/_locales/en/messages.json | 3 + .../popup/settings/settings-v2.component.html | 17 +- .../settings/settings-v2.component.spec.ts | 260 ++++++++++++++++++ .../popup/settings/settings-v2.component.ts | 43 ++- ...more-from-bitwarden-page-v2.component.html | 6 - .../more-from-bitwarden-page-v2.component.ts | 12 +- .../spotlight/spotlight.component.html | 16 +- .../spotlight/spotlight.component.spec.ts | 208 ++++++++++++++ .../spotlight/spotlight.component.ts | 35 +-- 9 files changed, 537 insertions(+), 63 deletions(-) create mode 100644 apps/browser/src/tools/popup/settings/settings-v2.component.spec.ts create mode 100644 libs/angular/src/vault/components/spotlight/spotlight.component.spec.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5cc7c30bfb4..14915175da1 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4902,6 +4902,9 @@ "premium": { "message": "Premium" }, + "unlockFeaturesWithPremium": { + "message": "Unlock reporting, emergency access, and more security features with Premium." + }, "freeOrgsCannotUseAttachments": { "message": "Free organizations cannot use attachments" }, diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.html b/apps/browser/src/tools/popup/settings/settings-v2.component.html index a12c5fe005f..683b7d70ed6 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.html +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.html @@ -1,4 +1,19 @@ + + {{ "unlockFeaturesWithPremium" | i18n }} + + + @@ -20,7 +35,7 @@

{{ "autofill" | i18n }}

{ + let account$: BehaviorSubject; + let mockAccountService: Partial; + let mockBillingState: { hasPremiumFromAnySource$: jest.Mock }; + let mockNudges: { + showNudgeBadge$: jest.Mock; + dismissNudge: jest.Mock; + }; + let mockAutofillSettings: { + defaultBrowserAutofillDisabled$: Subject; + isBrowserAutofillSettingOverridden: jest.Mock>; + }; + let dialogService: MockProxy; + let openSpy: jest.SpyInstance; + + beforeEach(waitForAsync(async () => { + dialogService = mock(); + account$ = new BehaviorSubject(null); + mockAccountService = { + activeAccount$: account$ as unknown as AccountService["activeAccount$"], + }; + + mockBillingState = { + hasPremiumFromAnySource$: jest.fn().mockReturnValue(of(false)), + }; + + mockNudges = { + showNudgeBadge$: jest.fn().mockImplementation(() => of(false)), + dismissNudge: jest.fn().mockResolvedValue(undefined), + }; + + mockAutofillSettings = { + defaultBrowserAutofillDisabled$: new BehaviorSubject(false), + isBrowserAutofillSettingOverridden: jest.fn().mockResolvedValue(false), + }; + + jest.spyOn(BrowserApi, "getBrowserClientVendor").mockReturnValue("Chrome"); + + const cfg = TestBed.configureTestingModule({ + imports: [SettingsV2Component, RouterTestingModule], + providers: [ + { provide: AccountService, useValue: mockAccountService }, + { provide: BillingAccountProfileStateService, useValue: mockBillingState }, + { provide: NudgesService, useValue: mockNudges }, + { provide: AutofillBrowserSettingsService, useValue: mockAutofillSettings }, + { provide: DialogService, useValue: dialogService }, + { provide: I18nService, useValue: { t: jest.fn((key: string) => key) } }, + { provide: GlobalStateProvider, useValue: new FakeGlobalStateProvider() }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: AvatarService, useValue: mock() }, + { provide: AuthService, useValue: mock() }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }); + + TestBed.overrideComponent(SettingsV2Component, { + add: { + imports: [CurrentAccountStubComponent], + providers: [{ provide: DialogService, useValue: dialogService }], + }, + remove: { + imports: [CurrentAccountComponent], + }, + }); + + await cfg.compileComponents(); + })); + + afterEach(() => { + jest.resetAllMocks(); + }); + + function pushActiveAccount(id = "user-123"): Account { + const acct = { id } as Account; + account$.next(acct); + return acct; + } + + it("shows the premium spotlight when user does NOT have premium", async () => { + mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(false)); + pushActiveAccount(); + + const fixture = TestBed.createComponent(SettingsV2Component); + fixture.detectChanges(); + await fixture.whenStable(); + + const el: HTMLElement = fixture.nativeElement; + + expect(el.querySelector("bit-spotlight")).toBeTruthy(); + }); + + it("hides the premium spotlight when user HAS premium", async () => { + mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(true)); + pushActiveAccount(); + + const fixture = TestBed.createComponent(SettingsV2Component); + fixture.detectChanges(); + await fixture.whenStable(); + + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector("bit-spotlight")).toBeFalsy(); + }); + + it("openUpgradeDialog calls PremiumUpgradeDialogComponent.open with the DialogService", async () => { + openSpy = jest.spyOn(PremiumUpgradeDialogComponent, "open").mockImplementation(); + mockBillingState.hasPremiumFromAnySource$.mockReturnValue(of(false)); + pushActiveAccount(); + + const fixture = TestBed.createComponent(SettingsV2Component); + const component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + component["openUpgradeDialog"](); + expect(openSpy).toHaveBeenCalledTimes(1); + expect(openSpy).toHaveBeenCalledWith(dialogService); + }); + + it("isBrowserAutofillSettingOverridden$ emits the value from the AutofillBrowserSettingsService", async () => { + pushActiveAccount(); + + mockAutofillSettings.isBrowserAutofillSettingOverridden.mockResolvedValue(true); + + const fixture = TestBed.createComponent(SettingsV2Component); + const component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + const value = await firstValueFrom(component["isBrowserAutofillSettingOverridden$"]); + expect(value).toBe(true); + + mockAutofillSettings.isBrowserAutofillSettingOverridden.mockResolvedValue(false); + + const fixture2 = TestBed.createComponent(SettingsV2Component); + const component2 = fixture2.componentInstance; + fixture2.detectChanges(); + await fixture2.whenStable(); + + const value2 = await firstValueFrom(component2["isBrowserAutofillSettingOverridden$"]); + expect(value2).toBe(false); + }); + + it("showAutofillBadge$ emits true when default autofill is NOT disabled and nudge is true", async () => { + pushActiveAccount(); + + mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) => + of(type === NudgeType.AutofillNudge), + ); + + const fixture = TestBed.createComponent(SettingsV2Component); + const component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + mockAutofillSettings.defaultBrowserAutofillDisabled$.next(false); + + const value = await firstValueFrom(component.showAutofillBadge$); + expect(value).toBe(true); + }); + + it("showAutofillBadge$ emits false when default autofill IS disabled even if nudge is true", async () => { + pushActiveAccount(); + + mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) => + of(type === NudgeType.AutofillNudge), + ); + + const fixture = TestBed.createComponent(SettingsV2Component); + const component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + mockAutofillSettings.defaultBrowserAutofillDisabled$.next(true); + + const value = await firstValueFrom(component.showAutofillBadge$); + expect(value).toBe(false); + }); + + it("dismissBadge dismisses when showVaultBadge$ emits true", async () => { + const acct = pushActiveAccount(); + + mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) => { + return of(type === NudgeType.EmptyVaultNudge); + }); + + const fixture = TestBed.createComponent(SettingsV2Component); + const component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + await component.dismissBadge(NudgeType.EmptyVaultNudge); + + expect(mockNudges.dismissNudge).toHaveBeenCalledTimes(1); + expect(mockNudges.dismissNudge).toHaveBeenCalledWith(NudgeType.EmptyVaultNudge, acct.id, true); + }); + + it("dismissBadge does nothing when showVaultBadge$ emits false", async () => { + pushActiveAccount(); + + mockNudges.showNudgeBadge$.mockReturnValue(of(false)); + + const fixture = TestBed.createComponent(SettingsV2Component); + const component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + await component.dismissBadge(NudgeType.EmptyVaultNudge); + + expect(mockNudges.dismissNudge).not.toHaveBeenCalled(); + }); + + it("showDownloadBitwardenNudge$ proxies to nudges service for the active account", async () => { + const acct = pushActiveAccount("user-xyz"); + + mockNudges.showNudgeBadge$.mockImplementation((type: NudgeType) => + of(type === NudgeType.DownloadBitwarden), + ); + + const fixture = TestBed.createComponent(SettingsV2Component); + const component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + const val = await firstValueFrom(component.showDownloadBitwardenNudge$); + expect(val).toBe(true); + expect(mockNudges.showNudgeBadge$).toHaveBeenCalledWith(NudgeType.DownloadBitwarden, acct.id); + }); +}); diff --git a/apps/browser/src/tools/popup/settings/settings-v2.component.ts b/apps/browser/src/tools/popup/settings/settings-v2.component.ts index 1c370381f54..95aeeb2f480 100644 --- a/apps/browser/src/tools/popup/settings/settings-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/settings-v2.component.ts @@ -1,21 +1,31 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; import { RouterModule } from "@angular/router"; import { combineLatest, filter, firstValueFrom, + from, map, Observable, shareReplay, switchMap, } from "rxjs"; +import { PremiumUpgradeDialogComponent } from "@bitwarden/angular/billing/components"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; +import { SpotlightComponent } from "@bitwarden/angular/vault/components/spotlight/spotlight.component"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { UserId } from "@bitwarden/common/types/guid"; -import { BadgeComponent, ItemModule } from "@bitwarden/components"; +import { + BadgeComponent, + DialogService, + ItemModule, + LinkModule, + TypographyModule, +} from "@bitwarden/components"; import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service"; @@ -24,8 +34,6 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ templateUrl: "settings-v2.component.html", imports: [ @@ -38,18 +46,30 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co ItemModule, CurrentAccountComponent, BadgeComponent, + SpotlightComponent, + TypographyModule, + LinkModule, ], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SettingsV2Component implements OnInit { +export class SettingsV2Component { NudgeType = NudgeType; - activeUserId: UserId | null = null; - protected isBrowserAutofillSettingOverridden = false; + + protected isBrowserAutofillSettingOverridden$ = from( + this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden( + BrowserApi.getBrowserClientVendor(window), + ), + ); private authenticatedAccount$: Observable = this.accountService.activeAccount$.pipe( filter((account): account is Account => account !== null), shareReplay({ bufferSize: 1, refCount: true }), ); + protected hasPremium$ = this.authenticatedAccount$.pipe( + switchMap((account) => this.accountProfileStateService.hasPremiumFromAnySource$(account.id)), + ); + showDownloadBitwardenNudge$: Observable = this.authenticatedAccount$.pipe( switchMap((account) => this.nudgesService.showNudgeBadge$(NudgeType.DownloadBitwarden, account.id), @@ -79,13 +99,12 @@ export class SettingsV2Component implements OnInit { private readonly nudgesService: NudgesService, private readonly accountService: AccountService, private readonly autofillBrowserSettingsService: AutofillBrowserSettingsService, + private readonly accountProfileStateService: BillingAccountProfileStateService, + private readonly dialogService: DialogService, ) {} - async ngOnInit() { - this.isBrowserAutofillSettingOverridden = - await this.autofillBrowserSettingsService.isBrowserAutofillSettingOverridden( - BrowserApi.getBrowserClientVendor(window), - ); + protected openUpgradeDialog() { + PremiumUpgradeDialogComponent.open(this.dialogService); } async dismissBadge(type: NudgeType) { diff --git a/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html index a2d01ce752e..a8ed75b5de6 100644 --- a/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html +++ b/apps/browser/src/vault/popup/settings/more-from-bitwarden-page-v2.component.html @@ -6,12 +6,6 @@ - - - {{ "premiumMembership" | i18n }} - - - ; protected familySponsorshipAvailable$: Observable; protected isFreeFamilyPolicyEnabled$: Observable; protected hasSingleEnterpriseOrg$: Observable; constructor( private dialogService: DialogService, - private billingAccountProfileStateService: BillingAccountProfileStateService, private environmentService: EnvironmentService, private organizationService: OrganizationService, private familiesPolicyService: FamiliesPolicyService, @@ -48,13 +45,6 @@ export class MoreFromBitwardenPageV2Component { this.familySponsorshipAvailable$ = getUserId(this.accountService.activeAccount$).pipe( switchMap((userId) => this.organizationService.familySponsorshipAvailable$(userId)), ); - this.canAccessPremium$ = this.accountService.activeAccount$.pipe( - switchMap((account) => - account - ? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) - : of(false), - ), - ); this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); } diff --git a/libs/angular/src/vault/components/spotlight/spotlight.component.html b/libs/angular/src/vault/components/spotlight/spotlight.component.html index 720bf5c1908..92b88eb967d 100644 --- a/libs/angular/src/vault/components/spotlight/spotlight.component.html +++ b/libs/angular/src/vault/components/spotlight/spotlight.component.html @@ -3,20 +3,20 @@ >
-

{{ title }}

+

{{ title() }}

- +
diff --git a/libs/angular/src/vault/components/spotlight/spotlight.component.spec.ts b/libs/angular/src/vault/components/spotlight/spotlight.component.spec.ts new file mode 100644 index 00000000000..3d4d35fdf63 --- /dev/null +++ b/libs/angular/src/vault/components/spotlight/spotlight.component.spec.ts @@ -0,0 +1,208 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { SpotlightComponent } from "./spotlight.component"; + +describe("SpotlightComponent", () => { + let fixture: ComponentFixture; + let component: SpotlightComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SpotlightComponent], + providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], + }).compileComponents(); + + fixture = TestBed.createComponent(SpotlightComponent); + component = fixture.componentInstance; + }); + + function detect(): void { + fixture.detectChanges(); + } + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + describe("rendering when inputs are null", () => { + it("should render without crashing when inputs are null/undefined", () => { + // Explicitly drive the inputs to null to exercise template null branches + fixture.componentRef.setInput("title", null); + fixture.componentRef.setInput("subtitle", null); + fixture.componentRef.setInput("buttonText", null); + fixture.componentRef.setInput("buttonIcon", null); + // persistent has a default, but drive it as well for coverage sanity + fixture.componentRef.setInput("persistent", false); + + expect(() => detect()).not.toThrow(); + + const root = fixture.debugElement.nativeElement as HTMLElement; + expect(root).toBeTruthy(); + }); + }); + + describe("close button visibility based on persistent", () => { + it("should show the close button when persistent is false", () => { + fixture.componentRef.setInput("persistent", false); + detect(); + + // Assumes dismiss uses bitIconButton + const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]")); + + expect(dismissButton).toBeTruthy(); + }); + + it("should hide the close button when persistent is true", () => { + fixture.componentRef.setInput("persistent", true); + detect(); + + const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]")); + expect(dismissButton).toBeNull(); + }); + }); + + describe("event emission", () => { + it("should emit onButtonClick when CTA button is clicked", () => { + const clickSpy = jest.fn(); + component.onButtonClick.subscribe(clickSpy); + + fixture.componentRef.setInput("buttonText", "Click me"); + detect(); + + const buttonDe = fixture.debugElement.query(By.css("button[bitButton]")); + expect(buttonDe).toBeTruthy(); + + const event = new MouseEvent("click"); + buttonDe.triggerEventHandler("click", event); + + expect(clickSpy).toHaveBeenCalledTimes(1); + expect(clickSpy.mock.calls[0][0]).toBeInstanceOf(MouseEvent); + }); + + it("should emit onDismiss when close button is clicked", () => { + const dismissSpy = jest.fn(); + component.onDismiss.subscribe(dismissSpy); + + fixture.componentRef.setInput("persistent", false); + detect(); + + const dismissButton = fixture.debugElement.query(By.css("button[bitIconButton]")); + expect(dismissButton).toBeTruthy(); + + dismissButton.triggerEventHandler("click", new MouseEvent("click")); + + expect(dismissSpy).toHaveBeenCalledTimes(1); + }); + + it("handleButtonClick should emit via onButtonClick()", () => { + const clickSpy = jest.fn(); + component.onButtonClick.subscribe(clickSpy); + + const event = new MouseEvent("click"); + component.handleButtonClick(event); + + expect(clickSpy).toHaveBeenCalledTimes(1); + expect(clickSpy.mock.calls[0][0]).toBe(event); + }); + + it("handleDismiss should emit via onDismiss()", () => { + const dismissSpy = jest.fn(); + component.onDismiss.subscribe(dismissSpy); + + component.handleDismiss(); + + expect(dismissSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("content projection behavior", () => { + @Component({ + standalone: true, + imports: [SpotlightComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + Projected content + + `, + }) + class HostWithProjectionComponent {} + + let hostFixture: ComponentFixture; + + beforeEach(async () => { + hostFixture = TestBed.createComponent(HostWithProjectionComponent); + }); + + it("should render projected content inside the spotlight", () => { + hostFixture.detectChanges(); + + const projected = hostFixture.debugElement.query(By.css(".tw-text-sm")); + expect(projected).toBeTruthy(); + expect(projected.nativeElement.textContent.trim()).toBe("Projected content"); + }); + }); + + describe("boolean attribute transform for persistent", () => { + @Component({ + standalone: true, + imports: [CommonModule, SpotlightComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + + + + + `, + }) + class BooleanHostComponent { + mode: "bare" | "none" | "falseStr" = "bare"; + } + + let boolFixture: ComponentFixture; + let boolHost: BooleanHostComponent; + + beforeEach(async () => { + boolFixture = TestBed.createComponent(BooleanHostComponent); + boolHost = boolFixture.componentInstance; + }); + + function getSpotlight(): SpotlightComponent { + const de = boolFixture.debugElement.query(By.directive(SpotlightComponent)); + return de.componentInstance as SpotlightComponent; + } + + it("treats bare 'persistent' attribute as true via booleanAttribute", () => { + boolHost.mode = "bare"; + boolFixture.detectChanges(); + + const spotlight = getSpotlight(); + expect(spotlight.persistent()).toBe(true); + }); + + it("uses default false when 'persistent' is omitted", () => { + boolHost.mode = "none"; + boolFixture.detectChanges(); + + const spotlight = getSpotlight(); + expect(spotlight.persistent()).toBe(false); + }); + + it('treats persistent="false" as false', () => { + boolHost.mode = "falseStr"; + boolFixture.detectChanges(); + + const spotlight = getSpotlight(); + expect(spotlight.persistent()).toBe(false); + }); + }); +}); diff --git a/libs/angular/src/vault/components/spotlight/spotlight.component.ts b/libs/angular/src/vault/components/spotlight/spotlight.component.ts index a912e4ce11b..1b75e1ee737 100644 --- a/libs/angular/src/vault/components/spotlight/spotlight.component.ts +++ b/libs/angular/src/vault/components/spotlight/spotlight.component.ts @@ -1,43 +1,28 @@ import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { booleanAttribute, ChangeDetectionStrategy, Component, input, output } from "@angular/core"; import { ButtonModule, IconButtonModule, TypographyModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush -// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ selector: "bit-spotlight", templateUrl: "spotlight.component.html", imports: [ButtonModule, CommonModule, IconButtonModule, I18nPipe, TypographyModule], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SpotlightComponent { // The title of the component - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input({ required: true }) title: string | null = null; + readonly title = input(); // The subtitle of the component - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() subtitle?: string | null = null; + readonly subtitle = input(); // The text to display on the button - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() buttonText?: string; - // Wheter the component can be dismissed, if true, the component will not show a close button - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() persistent = false; + readonly buttonText = input(); + // Whether the component can be dismissed, if true, the component will not show a close button + readonly persistent = input(false, { transform: booleanAttribute }); // Optional icon to display on the button - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-signals - @Input() buttonIcon: string | null = null; - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref - @Output() onDismiss = new EventEmitter(); - // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals - // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref - @Output() onButtonClick = new EventEmitter(); + readonly buttonIcon = input(); + readonly onDismiss = output(); + readonly onButtonClick = output(); handleButtonClick(event: MouseEvent): void { this.onButtonClick.emit(event); From e6d6f8d266d325289229b8ce139919aa38ba042f Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 25 Nov 2025 11:11:21 +0100 Subject: [PATCH 15/44] Migrate org reports to standalone and remove from loose components (#15791) --- .../exposed-passwords-report.component.ts | 6 +++++- .../inactive-two-factor-report.component.ts | 6 +++++- .../reused-passwords-report.component.ts | 6 +++++- .../unsecured-websites-report.component.ts | 6 +++++- .../weak-passwords-report.component.ts | 6 +++++- .../web/src/app/shared/loose-components.module.ts | 15 --------------- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts index 4dbd31ce4dc..f83614557bd 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/exposed-passwords-report.component.ts @@ -19,6 +19,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService, CipherFormConfigService } from "@bitwarden/vault"; +import { HeaderModule } from "../../../../layouts/header/header.module"; +import { SharedModule } from "../../../../shared"; +import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -38,7 +42,7 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], - standalone: false, + imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], }) export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts index 17555e617cb..b1adbd26eb3 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/inactive-two-factor-report.component.ts @@ -14,6 +14,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { HeaderModule } from "../../../../layouts/header/header.module"; +import { SharedModule } from "../../../../shared"; +import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -32,7 +36,7 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], - standalone: false, + imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], }) export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts index 5e457a91bd9..3944e2edfcb 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/reused-passwords-report.component.ts @@ -18,6 +18,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { HeaderModule } from "../../../../layouts/header/header.module"; +import { SharedModule } from "../../../../shared"; +import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -37,7 +41,7 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], - standalone: false, + imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], }) export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts index 24f514d551f..d49baa5d465 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/unsecured-websites-report.component.ts @@ -18,6 +18,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { HeaderModule } from "../../../../layouts/header/header.module"; +import { SharedModule } from "../../../../shared"; +import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -37,7 +41,7 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], - standalone: false, + imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], }) export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent diff --git a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts index 50c18d1da3b..5158416dd28 100644 --- a/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts +++ b/apps/web/src/app/dirt/reports/pages/organizations/weak-passwords-report.component.ts @@ -19,6 +19,10 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault"; +import { HeaderModule } from "../../../../layouts/header/header.module"; +import { SharedModule } from "../../../../shared"; +import { OrganizationBadgeModule } from "../../../../vault/individual-vault/organization-badge/organization-badge.module"; +import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module"; import { RoutedVaultFilterBridgeService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/routed-vault-filter.service"; import { AdminConsoleCipherFormConfigService } from "../../../../vault/org-vault/services/admin-console-cipher-form-config.service"; @@ -38,7 +42,7 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from RoutedVaultFilterService, RoutedVaultFilterBridgeService, ], - standalone: false, + imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], }) export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index f7f3aa3bfee..0fff13f428c 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -7,16 +7,6 @@ import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.comp import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component"; import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component"; -// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module -import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../dirt/reports/pages/organizations/exposed-passwords-report.component"; -// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module -import { InactiveTwoFactorReportComponent as OrgInactiveTwoFactorReportComponent } from "../dirt/reports/pages/organizations/inactive-two-factor-report.component"; -// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module -import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } from "../dirt/reports/pages/organizations/reused-passwords-report.component"; -// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module -import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../dirt/reports/pages/organizations/unsecured-websites-report.component"; -// eslint-disable-next-line no-restricted-imports -- Temporarily disabled until DIRT refactors these out of this module -import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../dirt/reports/pages/organizations/weak-passwords-report.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { HeaderModule } from "../layouts/header/header.module"; import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; @@ -29,11 +19,6 @@ import { SharedModule } from "./shared.module"; @NgModule({ imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], declarations: [ - OrgExposedPasswordsReportComponent, - OrgInactiveTwoFactorReportComponent, - OrgReusedPasswordsReportComponent, - OrgUnsecuredWebsitesReportComponent, - OrgWeakPasswordsReportComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, From 86a757119c31e414f1376c2dd8d1d80e63aaad13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:07:02 +0100 Subject: [PATCH 16/44] [deps] Architecture: Update @eslint/compat to v2 (#17622) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Oscar Hinton --- package-lock.json | 28 ++++++++++++++++++++++------ package.json | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ec580e3a5a..39256cdbb97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,7 +84,7 @@ "@compodoc/compodoc": "1.1.26", "@electron/notarize": "3.0.1", "@electron/rebuild": "4.0.1", - "@eslint/compat": "1.2.9", + "@eslint/compat": "2.0.0", "@lit-labs/signals": "0.1.2", "@ngtools/webpack": "19.2.14", "@storybook/addon-a11y": "8.6.12", @@ -6559,16 +6559,19 @@ } }, "node_modules/@eslint/compat": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.9.tgz", - "integrity": "sha512-gCdSY54n7k+driCadyMNv8JSPzYLeDVM/ikZRtvtROBpRdFSkS8W9A82MqsaY7lZuwL0wiapgD0NT1xT0hyJsA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.0.tgz", + "integrity": "sha512-T9AfE1G1uv4wwq94ozgTGio5EUQBqAVe1X9qsQtSNVEYW6j3hvtZVm8Smr4qL1qDPFg+lOB2cL5RxTRMzq4CTA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.0.0" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "peerDependencies": { - "eslint": "^9.10.0" + "eslint": "^8.40 || 9" }, "peerDependenciesMeta": { "eslint": { @@ -6576,6 +6579,19 @@ } } }, + "node_modules/@eslint/compat/node_modules/@eslint/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.0.tgz", + "integrity": "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@eslint/config-array": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", diff --git a/package.json b/package.json index 87a78a30796..337a3caa3bc 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@compodoc/compodoc": "1.1.26", "@electron/notarize": "3.0.1", "@electron/rebuild": "4.0.1", - "@eslint/compat": "1.2.9", + "@eslint/compat": "2.0.0", "@lit-labs/signals": "0.1.2", "@ngtools/webpack": "19.2.14", "@storybook/addon-a11y": "8.6.12", From 9e90e72961663c7042e5b14165db921ad6afc874 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 25 Nov 2025 14:48:25 +0100 Subject: [PATCH 17/44] [PM-27530] Rename BitwardenClient to PasswordManagerClient (#17578) * fix: compilation issues with PM client rename * fix: jest compilation * feat: rename all non-breaking platform instances * feat: update SDK --- libs/common/spec/jest-sdk-client-factory.ts | 6 ++--- .../abstractions/sdk/sdk-client-factory.ts | 10 ++++----- .../platform/abstractions/sdk/sdk.service.ts | 8 +++---- .../sdk/default-sdk-client-factory.ts | 12 +++++----- .../services/sdk/default-sdk.service.spec.ts | 18 +++++++-------- .../services/sdk/default-sdk.service.ts | 22 +++++++++---------- .../services/sdk/noop-sdk-client-factory.ts | 6 ++--- .../src/platform/spec/mock-sdk.service.ts | 14 ++++++------ package-lock.json | 16 +++++++------- package.json | 4 ++-- 10 files changed, 58 insertions(+), 58 deletions(-) diff --git a/libs/common/spec/jest-sdk-client-factory.ts b/libs/common/spec/jest-sdk-client-factory.ts index 8e5e1c9d3fc..8b93ba791b3 100644 --- a/libs/common/spec/jest-sdk-client-factory.ts +++ b/libs/common/spec/jest-sdk-client-factory.ts @@ -1,11 +1,11 @@ -import { BitwardenClient } from "@bitwarden/sdk-internal"; +import { PasswordManagerClient } from "@bitwarden/sdk-internal"; import { SdkClientFactory } from "../src/platform/abstractions/sdk/sdk-client-factory"; export class DefaultSdkClientFactory implements SdkClientFactory { createSdkClient( - ...args: ConstructorParameters - ): Promise { + ...args: ConstructorParameters + ): Promise { throw new Error("Method not implemented."); } } diff --git a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts index 6a1b7b67b42..35830e42f16 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk-client-factory.ts @@ -1,14 +1,14 @@ -import type { BitwardenClient } from "@bitwarden/sdk-internal"; +import type { PasswordManagerClient } from "@bitwarden/sdk-internal"; /** * Factory for creating SDK clients. */ export abstract class SdkClientFactory { /** - * Creates a new BitwardenClient. Assumes the SDK is already loaded. - * @param args Bitwarden client constructor parameters + * Creates a new Password Manager client. Assumes the SDK is already loaded. + * @param args Password Manager client constructor parameters */ abstract createSdkClient( - ...args: ConstructorParameters - ): Promise; + ...args: ConstructorParameters + ): Promise; } diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index 03baec5cc37..9b7f32a8a0e 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { BitwardenClient, Uuid } from "@bitwarden/sdk-internal"; +import { PasswordManagerClient, Uuid } from "@bitwarden/sdk-internal"; import { UserId } from "../../../types/guid"; import { Rc } from "../../misc/reference-counting/rc"; @@ -46,7 +46,7 @@ export abstract class SdkService { * Retrieve a client initialized without a user. * This client can only be used for operations that don't require a user context. */ - abstract client$: Observable; + abstract client$: Observable; /** * Retrieve a client initialized for a specific user. @@ -64,7 +64,7 @@ export abstract class SdkService { * * @param userId The user id for which to retrieve the client */ - abstract userClient$(userId: UserId): Observable>; + abstract userClient$(userId: UserId): Observable>; /** * This method is used during/after an authentication procedure to set a new client for a specific user. @@ -75,5 +75,5 @@ export abstract class SdkService { * @param userId The user id for which to set the client * @param client The client to set for the user. If undefined, the client will be unset. */ - abstract setClient(userId: UserId, client: BitwardenClient | undefined): void; + abstract setClient(userId: UserId, client: PasswordManagerClient | undefined): void; } diff --git a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts index fc55cc83ac8..d0e4b96ba89 100644 --- a/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts +++ b/libs/common/src/platform/services/sdk/default-sdk-client-factory.ts @@ -7,13 +7,13 @@ import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; */ export class DefaultSdkClientFactory implements SdkClientFactory { /** - * Initializes a Bitwarden client. Assumes the SDK is already loaded. - * @param args Bitwarden client constructor parameters - * @returns A BitwardenClient + * Initializes a Password Manager client. Assumes the SDK is already loaded. + * @param args Password Manager client constructor parameters + * @returns A PasswordManagerClient */ async createSdkClient( - ...args: ConstructorParameters - ): Promise { - return Promise.resolve(new sdk.BitwardenClient(...args)); + ...args: ConstructorParameters + ): Promise { + return Promise.resolve(new sdk.PasswordManagerClient(...args)); } } diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index 769e8521d88..dc945594079 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -5,7 +5,7 @@ import { SecurityStateService } from "@bitwarden/common/key-management/security- // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { KdfConfigService, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; +import { PasswordManagerClient } from "@bitwarden/sdk-internal"; import { ObservableTracker, @@ -109,7 +109,7 @@ describe("DefaultSdkService", () => { }); describe("given no client override has been set for the user", () => { - let mockClient!: MockProxy; + let mockClient!: MockProxy; beforeEach(() => { mockClient = createMockClient(); @@ -123,8 +123,8 @@ describe("DefaultSdkService", () => { }); it("does not create an SDK client when called the second time with same userId", async () => { - const subject_1 = new BehaviorSubject | undefined>(undefined); - const subject_2 = new BehaviorSubject | undefined>(undefined); + const subject_1 = new BehaviorSubject | undefined>(undefined); + const subject_2 = new BehaviorSubject | undefined>(undefined); // Use subjects to ensure the subscription is kept alive service.userClient$(userId).subscribe(subject_1); @@ -139,8 +139,8 @@ describe("DefaultSdkService", () => { }); it("destroys the internal SDK client when all subscriptions are closed", async () => { - const subject_1 = new BehaviorSubject | undefined>(undefined); - const subject_2 = new BehaviorSubject | undefined>(undefined); + const subject_1 = new BehaviorSubject | undefined>(undefined); + const subject_2 = new BehaviorSubject | undefined>(undefined); const subscription_1 = service.userClient$(userId).subscribe(subject_1); const subscription_2 = service.userClient$(userId).subscribe(subject_2); await new Promise(process.nextTick); @@ -170,7 +170,7 @@ describe("DefaultSdkService", () => { describe("given overrides are used", () => { it("does not create a new client and emits the override client when a client override has already been set ", async () => { - const mockClient = mock(); + const mockClient = mock(); service.setClient(userId, mockClient); const userClientTracker = new ObservableTracker(service.userClient$(userId), false); await userClientTracker.pauseUntilReceived(1); @@ -242,8 +242,8 @@ describe("DefaultSdkService", () => { }); }); -function createMockClient(): MockProxy { - const client = mock(); +function createMockClient(): MockProxy { + const client = mock(); client.crypto.mockReturnValue(mock()); client.platform.mockReturnValue({ state: jest.fn().mockReturnValue(mock()), diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index 6f9c9df761c..eb663c6f928 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -20,7 +20,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co // eslint-disable-next-line no-restricted-imports import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key-management"; import { - BitwardenClient, + PasswordManagerClient, ClientSettings, DeviceType as SdkDeviceType, TokenProvider, @@ -70,9 +70,9 @@ class JsTokenProvider implements TokenProvider { export class DefaultSdkService implements SdkService { private sdkClientOverrides = new BehaviorSubject<{ - [userId: UserId]: Rc | typeof UnsetClient; + [userId: UserId]: Rc | typeof UnsetClient; }>({}); - private sdkClientCache = new Map>>(); + private sdkClientCache = new Map>>(); client$ = this.environmentService.environment$.pipe( concatMap(async (env) => { @@ -107,14 +107,14 @@ export class DefaultSdkService implements SdkService { private userAgent: string | null = null, ) {} - userClient$(userId: UserId): Observable> { + userClient$(userId: UserId): Observable> { return this.sdkClientOverrides.pipe( takeWhile((clients) => clients[userId] !== UnsetClient, false), map((clients) => { if (clients[userId] === UnsetClient) { throw new Error("Encountered UnsetClient even though it should have been filtered out"); } - return clients[userId] as Rc; + return clients[userId] as Rc; }), distinctUntilChanged(), switchMap((clientOverride) => { @@ -129,7 +129,7 @@ export class DefaultSdkService implements SdkService { ); } - setClient(userId: UserId, client: BitwardenClient | undefined) { + setClient(userId: UserId, client: PasswordManagerClient | undefined) { const previousValue = this.sdkClientOverrides.value[userId]; this.sdkClientOverrides.next({ @@ -149,7 +149,7 @@ export class DefaultSdkService implements SdkService { * @param userId The user id for which to create the client * @returns An observable that emits the client for the user */ - private internalClient$(userId: UserId): Observable> { + private internalClient$(userId: UserId): Observable> { const cached = this.sdkClientCache.get(userId); if (cached !== undefined) { return cached; @@ -187,7 +187,7 @@ export class DefaultSdkService implements SdkService { switchMap( ([env, account, kdfParams, privateKey, userKey, signingKey, orgKeys, securityState]) => { // Create our own observable to be able to implement clean-up logic - return new Observable>((subscriber) => { + return new Observable>((subscriber) => { const createAndInitializeClient = async () => { if (env == null || kdfParams == null || privateKey == null || userKey == null) { return undefined; @@ -214,7 +214,7 @@ export class DefaultSdkService implements SdkService { return client; }; - let client: Rc | undefined; + let client: Rc | undefined; createAndInitializeClient() .then((c) => { client = c === undefined ? undefined : new Rc(c); @@ -239,7 +239,7 @@ export class DefaultSdkService implements SdkService { private async initializeClient( userId: UserId, - client: BitwardenClient, + client: PasswordManagerClient, account: AccountInfo, kdfParams: KdfConfig, privateKey: EncryptedString, @@ -281,7 +281,7 @@ export class DefaultSdkService implements SdkService { await this.loadFeatureFlags(client); } - private async loadFeatureFlags(client: BitwardenClient) { + private async loadFeatureFlags(client: PasswordManagerClient) { const serverConfig = await firstValueFrom(this.configService.serverConfig$); const featureFlagMap = new Map( diff --git a/libs/common/src/platform/services/sdk/noop-sdk-client-factory.ts b/libs/common/src/platform/services/sdk/noop-sdk-client-factory.ts index d7eab7e8dc9..8ed0bc276cc 100644 --- a/libs/common/src/platform/services/sdk/noop-sdk-client-factory.ts +++ b/libs/common/src/platform/services/sdk/noop-sdk-client-factory.ts @@ -1,4 +1,4 @@ -import type { BitwardenClient } from "@bitwarden/sdk-internal"; +import type { PasswordManagerClient } from "@bitwarden/sdk-internal"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; @@ -9,8 +9,8 @@ import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; */ export class NoopSdkClientFactory implements SdkClientFactory { createSdkClient( - ...args: ConstructorParameters - ): Promise { + ...args: ConstructorParameters + ): Promise { return Promise.reject(new Error("SDK not available")); } } diff --git a/libs/common/src/platform/spec/mock-sdk.service.ts b/libs/common/src/platform/spec/mock-sdk.service.ts index 66a6ab3ec84..aec2438c853 100644 --- a/libs/common/src/platform/spec/mock-sdk.service.ts +++ b/libs/common/src/platform/spec/mock-sdk.service.ts @@ -7,7 +7,7 @@ import { throwIfEmpty, } from "rxjs"; -import { BitwardenClient } from "@bitwarden/sdk-internal"; +import { PasswordManagerClient } from "@bitwarden/sdk-internal"; import { UserId } from "../../types/guid"; import { SdkService, UserNotLoggedInError } from "../abstractions/sdk/sdk.service"; @@ -17,18 +17,18 @@ import { DeepMockProxy, mockDeep } from "./mock-deep"; export class MockSdkService implements SdkService { private userClients$ = new BehaviorSubject<{ - [userId: UserId]: Rc | undefined; + [userId: UserId]: Rc | undefined; }>({}); - private _client$ = new BehaviorSubject(mockDeep()); + private _client$ = new BehaviorSubject(mockDeep()); client$ = this._client$.asObservable(); version$ = new BehaviorSubject("0.0.1-test").asObservable(); - userClient$(userId: UserId): Observable> { + userClient$(userId: UserId): Observable> { return this.userClients$.pipe( takeWhile((clients) => clients[userId] !== undefined, false), - map((clients) => clients[userId] as Rc), + map((clients) => clients[userId] as Rc), distinctUntilChanged(), throwIfEmpty(() => new UserNotLoggedInError(userId)), ); @@ -42,7 +42,7 @@ export class MockSdkService implements SdkService { * Returns the non-user scoped client mock. * This is what is returned by the `client$` observable. */ - get client(): DeepMockProxy { + get client(): DeepMockProxy { return this._client$.value; } @@ -55,7 +55,7 @@ export class MockSdkService implements SdkService { * @returns A user-scoped mock for the user. */ userLogin: (userId: UserId) => { - const client = mockDeep(); + const client = mockDeep(); this.userClients$.next({ ...this.userClients$.getValue(), [userId]: new Rc(client), diff --git a/package-lock.json b/package-lock.json index 39256cdbb97..1e2b8119a95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,8 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.375", - "@bitwarden/sdk-internal": "0.2.0-main.375", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.395", + "@bitwarden/sdk-internal": "0.2.0-main.395", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4620,9 +4620,9 @@ "link": true }, "node_modules/@bitwarden/commercial-sdk-internal": { - "version": "0.2.0-main.375", - "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.375.tgz", - "integrity": "sha512-UMVfLjMh79+5et1if7qqOi+pSGP5Ay3AcGp4E5oLZ0p0yFsN2Q54UFv+SLju0/oI0qTvVZP1RkEtTJXHdNrpTg==", + "version": "0.2.0-main.395", + "resolved": "https://registry.npmjs.org/@bitwarden/commercial-sdk-internal/-/commercial-sdk-internal-0.2.0-main.395.tgz", + "integrity": "sha512-DrxL3iA29hzWpyxPyZjiXx0m+EHOgk4CVb+BAi2SoxsacmyHYuTgXuASFMieRz2rv85wS3UR0N64Ok9lC+xNYA==", "license": "BITWARDEN SOFTWARE DEVELOPMENT KIT LICENSE AGREEMENT", "dependencies": { "type-fest": "^4.41.0" @@ -4725,9 +4725,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.375", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.375.tgz", - "integrity": "sha512-kf2SKFkAdSmV2/ORo6u1eegwYW2ha62NHUsx2ij2uPWmm7mzXUoNa7z8mqhJV1ozg5o7yBqBuXd6Wqo9Ww+/RA==", + "version": "0.2.0-main.395", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.395.tgz", + "integrity": "sha512-biExeL2Grp11VQjjK6QM16+WOYk87mTgUhYKFm+Bu/A0zZBzhL/6AocpA9h2T5M8rLCGVVJVUMaXUW3YrSTqEA==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index 337a3caa3bc..2a06b80f007 100644 --- a/package.json +++ b/package.json @@ -160,8 +160,8 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.375", - "@bitwarden/commercial-sdk-internal": "0.2.0-main.375", + "@bitwarden/sdk-internal": "0.2.0-main.395", + "@bitwarden/commercial-sdk-internal": "0.2.0-main.395", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From cdd8a697e8687fb14c2d7899c45919ce481eccac Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:41:41 -0600 Subject: [PATCH 18/44] do not show copy password button on the web for users that do not have access (#17635) --- .../vault-cipher-row.component.html | 10 +- .../vault-cipher-row.component.spec.ts | 144 ++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.spec.ts diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index c09553dab9c..c8732154ef4 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -109,10 +109,12 @@ {{ "copyUsername" | i18n }} - + @if (cipher.viewPassword) { + + }
{{ cipher.name }} - @if (cipher.hasAttachments) { + @if (CipherViewLikeUtils.hasAttachments(cipher)) { } - {{ cipher.subTitle }} + {{ CipherViewLikeUtils.subtitle(cipher) }} @@ -45,7 +45,7 @@ type="button" bitMenuItem (click)="restore(cipher)" - *ngIf="!cipher.decryptionFailure" + *ngIf="!hasDecryptionFailure(cipher)" > {{ "restore" | i18n }} diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index 70ba6842a0d..bad6011b2d8 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -12,7 +12,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService, IconButtonModule, @@ -85,10 +84,40 @@ export class TrashListItemsContainerComponent { return collections[0]?.name; } - async restore(cipher: CipherView) { + /** + * Check if a cipher has attachments. CipherView has a hasAttachments getter, + * while CipherListView has an attachments count property. + */ + hasAttachments(cipher: PopupCipherViewLike): boolean { + if ("hasAttachments" in cipher) { + return cipher.hasAttachments; + } + return cipher.attachments > 0; + } + + /** + * Get the subtitle for a cipher. CipherView has a subTitle getter, + * while CipherListView has a subtitle property. + */ + getSubtitle(cipher: PopupCipherViewLike): string | undefined { + if ("subTitle" in cipher) { + return cipher.subTitle; + } + return cipher.subtitle; + } + + /** + * Check if a cipher has a decryption failure. CipherView has this property, + * while CipherListView does not. + */ + hasDecryptionFailure(cipher: PopupCipherViewLike): boolean { + return "decryptionFailure" in cipher && cipher.decryptionFailure; + } + + async restore(cipher: PopupCipherViewLike) { try { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.cipherService.restoreWithServer(cipher.id, activeUserId); + await this.cipherService.restoreWithServer(cipher.id as string, activeUserId); await this.router.navigate(["/trash"]); this.toastService.showToast({ @@ -101,7 +130,7 @@ export class TrashListItemsContainerComponent { } } - async delete(cipher: CipherView) { + async delete(cipher: PopupCipherViewLike) { const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); if (!repromptPassed) { @@ -120,7 +149,7 @@ export class TrashListItemsContainerComponent { try { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.cipherService.deleteWithServer(cipher.id, activeUserId); + await this.cipherService.deleteWithServer(cipher.id as string, activeUserId); await this.router.navigate(["/trash"]); this.toastService.showToast({ @@ -133,8 +162,9 @@ export class TrashListItemsContainerComponent { } } - async onViewCipher(cipher: CipherView) { - if (cipher.decryptionFailure) { + async onViewCipher(cipher: PopupCipherViewLike) { + // CipherListView doesn't have decryptionFailure, so we use optional chaining + if ("decryptionFailure" in cipher && cipher.decryptionFailure) { DecryptionFailureDialogComponent.open(this.dialogService, { cipherIds: [cipher.id as CipherId], }); @@ -147,7 +177,7 @@ export class TrashListItemsContainerComponent { } await this.router.navigate(["/view-cipher"], { - queryParams: { cipherId: cipher.id, type: cipher.type }, + queryParams: { cipherId: cipher.id as string, type: cipher.type }, }); } } diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 0fd6cac4230..6fb9dfbe46b 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "../../tsconfig.base", + "angularCompilerOptions": { + "strictTemplates": true + }, "include": [ "src", "../../libs/common/src/autofill/constants", diff --git a/libs/vault/src/components/can-delete-cipher.directive.ts b/libs/vault/src/components/can-delete-cipher.directive.ts index 8ab59f9d647..f88e38049da 100644 --- a/libs/vault/src/components/can-delete-cipher.directive.ts +++ b/libs/vault/src/components/can-delete-cipher.directive.ts @@ -1,8 +1,8 @@ import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from "@angular/core"; import { Subject, takeUntil } from "rxjs"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; +import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; /** * Only shows the element if the user can delete the cipher. @@ -15,7 +15,7 @@ export class CanDeleteCipherDirective implements OnDestroy { // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals - @Input("appCanDeleteCipher") set cipher(cipher: CipherView) { + @Input("appCanDeleteCipher") set cipher(cipher: CipherViewLike) { this.viewContainer.clear(); this.cipherAuthorizationService diff --git a/libs/vault/src/components/copy-cipher-field.directive.ts b/libs/vault/src/components/copy-cipher-field.directive.ts index b4b1941273f..52a4f59e7a2 100644 --- a/libs/vault/src/components/copy-cipher-field.directive.ts +++ b/libs/vault/src/components/copy-cipher-field.directive.ts @@ -36,7 +36,7 @@ export class CopyCipherFieldDirective implements OnChanges { alias: "appCopyField", required: true, }) - action!: Exclude; + action!: CopyAction; // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-signals diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index ccd830cd34e..93a72ba14e0 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -3,7 +3,11 @@ export { AtRiskPasswordCalloutData, } from "./services/at-risk-password-callout.service"; export { PasswordRepromptService } from "./services/password-reprompt.service"; -export { CopyCipherFieldService, CopyAction } from "./services/copy-cipher-field.service"; +export { + CopyCipherFieldService, + CopyAction, + CopyFieldAction, +} from "./services/copy-cipher-field.service"; export { CopyCipherFieldDirective } from "./components/copy-cipher-field.directive"; export { OrgIconDirective } from "./components/org-icon.directive"; export { CanDeleteCipherDirective } from "./components/can-delete-cipher.directive"; diff --git a/libs/vault/src/services/copy-cipher-field.service.ts b/libs/vault/src/services/copy-cipher-field.service.ts index 0a7d9e1f68d..539c81b7be3 100644 --- a/libs/vault/src/services/copy-cipher-field.service.ts +++ b/libs/vault/src/services/copy-cipher-field.service.ts @@ -35,6 +35,12 @@ export type CopyAction = | "publicKey" | "keyFingerprint"; +/** + * Copy actions that can be used with the appCopyField directive. + * Excludes "hiddenField" which requires special handling. + */ +export type CopyFieldAction = Exclude; + type CopyActionInfo = { /** * The i18n key for the type of field being copied. Will be used to display a toast message. From c04c1757ea6de993a6c808fdda7d64cdf0651425 Mon Sep 17 00:00:00 2001 From: Ben Brooks <56796209+bensbits91@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:06:03 -0800 Subject: [PATCH 23/44] Revert "Lets shadow DOM check signal page update (#16114)" (commit 6129ca536686cfceb385e9b87f6c569f0ecf9558) (#17503) Signed-off-by: Ben Brooks --- .../abstractions/dom-query.service.ts | 2 +- .../collect-autofill-content.service.spec.ts | 31 +------------------ .../collect-autofill-content.service.ts | 13 ++------ .../services/dom-query.service.spec.ts | 2 ++ .../autofill/services/dom-query.service.ts | 9 ++++-- 5 files changed, 13 insertions(+), 44 deletions(-) diff --git a/apps/browser/src/autofill/services/abstractions/dom-query.service.ts b/apps/browser/src/autofill/services/abstractions/dom-query.service.ts index 32809573223..da7354403e5 100644 --- a/apps/browser/src/autofill/services/abstractions/dom-query.service.ts +++ b/apps/browser/src/autofill/services/abstractions/dom-query.service.ts @@ -6,5 +6,5 @@ export interface DomQueryService { mutationObserver?: MutationObserver, forceDeepQueryAttempt?: boolean, ): T[]; - checkPageContainsShadowDom(): boolean; + checkPageContainsShadowDom(): void; } diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts index 9ee329fa150..66a692dbe20 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.spec.ts @@ -395,7 +395,7 @@ describe("CollectAutofillContentService", () => { }); }); - it("sets the noFieldsFound property to true if the page has no forms or fields", async function () { + it("sets the noFieldsFond property to true if the page has no forms or fields", async function () { document.body.innerHTML = ""; collectAutofillContentService["noFieldsFound"] = false; jest.spyOn(collectAutofillContentService as any, "buildAutofillFormsData"); @@ -2649,33 +2649,4 @@ describe("CollectAutofillContentService", () => { ); }); }); - - describe("processMutations", () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); - }); - - it("will require an update to page details if shadow DOM is present", () => { - jest - .spyOn(domQueryService as any, "checkPageContainsShadowDom") - .mockImplementationOnce(() => true); - - collectAutofillContentService["requirePageDetailsUpdate"] = jest.fn(); - - collectAutofillContentService["mutationsQueue"] = [[], []]; - - collectAutofillContentService["processMutations"](); - - jest.runOnlyPendingTimers(); - - expect(domQueryService.checkPageContainsShadowDom).toHaveBeenCalled(); - expect(collectAutofillContentService["mutationsQueue"]).toHaveLength(0); - expect(collectAutofillContentService["requirePageDetailsUpdate"]).toHaveBeenCalled(); - }); - }); }); diff --git a/apps/browser/src/autofill/services/collect-autofill-content.service.ts b/apps/browser/src/autofill/services/collect-autofill-content.service.ts index 47b1c9ea6df..6f2c00a4dd4 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -997,13 +997,6 @@ export class CollectAutofillContentService implements CollectAutofillContentServ * within an idle callback to help with performance and prevent excessive updates. */ private processMutations = () => { - // If the page contains shadow DOM, we require a page details update from the autofill service. - // Will wait for an idle moment on main thread to execute, unless timeout has passed. - requestIdleCallbackPolyfill( - () => this.domQueryService.checkPageContainsShadowDom() && this.requirePageDetailsUpdate(), - { timeout: 500 }, - ); - const queueLength = this.mutationsQueue.length; for (let queueIndex = 0; queueIndex < queueLength; queueIndex++) { @@ -1026,13 +1019,13 @@ export class CollectAutofillContentService implements CollectAutofillContentServ * 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 requirePageDetailsUpdate = () => { + private flagPageDetailsUpdateIsRequired() { this.domRecentlyMutated = true; if (this.autofillOverlayContentService) { this.autofillOverlayContentService.pageDetailsUpdateRequired = true; } this.noFieldsFound = false; - }; + } /** * Processes all mutation records encountered by the mutation observer. @@ -1060,7 +1053,7 @@ export class CollectAutofillContentService implements CollectAutofillContentServ (this.isAutofillElementNodeMutated(mutation.removedNodes, true) || this.isAutofillElementNodeMutated(mutation.addedNodes)) ) { - this.requirePageDetailsUpdate(); + this.flagPageDetailsUpdateIsRequired(); return; } diff --git a/apps/browser/src/autofill/services/dom-query.service.spec.ts b/apps/browser/src/autofill/services/dom-query.service.spec.ts index 87645c98a45..53862aef735 100644 --- a/apps/browser/src/autofill/services/dom-query.service.spec.ts +++ b/apps/browser/src/autofill/services/dom-query.service.spec.ts @@ -72,6 +72,7 @@ describe("DomQueryService", () => { }); it("queries form field elements that are nested within multiple ShadowDOM elements", () => { + domQueryService["pageContainsShadowDom"] = true; const root = document.createElement("div"); const shadowRoot1 = root.attachShadow({ mode: "open" }); const root2 = document.createElement("div"); @@ -94,6 +95,7 @@ describe("DomQueryService", () => { }); it("will fallback to using the TreeWalker API if a depth larger than 4 ShadowDOM elements is encountered", () => { + domQueryService["pageContainsShadowDom"] = true; const root = document.createElement("div"); const shadowRoot1 = root.attachShadow({ mode: "open" }); const root2 = document.createElement("div"); diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts index 1b0c5681ff0..932bbe47f90 100644 --- a/apps/browser/src/autofill/services/dom-query.service.ts +++ b/apps/browser/src/autofill/services/dom-query.service.ts @@ -78,9 +78,8 @@ export class DomQueryService implements DomQueryServiceInterface { /** * Checks if the page contains any shadow DOM elements. */ - checkPageContainsShadowDom = (): boolean => { + checkPageContainsShadowDom = (): void => { this.pageContainsShadowDom = this.queryShadowRoots(globalThis.document.body, true).length > 0; - return this.pageContainsShadowDom; }; /** @@ -109,7 +108,7 @@ export class DomQueryService implements DomQueryServiceInterface { ): T[] { let elements = this.queryElements(root, queryString); - const shadowRoots = this.pageContainsShadowDom ? this.recursivelyQueryShadowRoots(root) : []; + const shadowRoots = this.recursivelyQueryShadowRoots(root); for (let index = 0; index < shadowRoots.length; index++) { const shadowRoot = shadowRoots[index]; elements = elements.concat(this.queryElements(shadowRoot, queryString)); @@ -152,6 +151,10 @@ export class DomQueryService implements DomQueryServiceInterface { root: Document | ShadowRoot | Element, depth: number = 0, ): ShadowRoot[] { + if (!this.pageContainsShadowDom) { + return []; + } + if (depth >= MAX_DEEP_QUERY_RECURSION_DEPTH) { throw new Error("Max recursion depth reached"); } From cf6569bfea7516cf3e86f2854d5c61c17a547983 Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:23:22 -0500 Subject: [PATCH 24/44] feat(user-decryption-options) [PM-26413]: Remove ActiveUserState from UserDecryptionOptionsService (#16894) * feat(user-decryption-options) [PM-26413]: Update UserDecryptionOptionsService and tests to use UserId-only APIs. * feat(user-decryption-options) [PM-26413]: Update InternalUserDecryptionOptionsService call sites to use UserId-only API. * feat(user-decryption-options) [PM-26413] Update userDecryptionOptions$ call sites to use the UserId-only API. * feat(user-decryption-options) [PM-26413]: Update additional call sites. * feat(user-decryption-options) [PM-26413]: Update dependencies and an additional call site. * feat(user-verification-service) [PM-26413]: Replace where allowed by unrestricted imports invocation of UserVerificationService.hasMasterPassword (deprecated) with UserDecryptionOptions.hasMasterPasswordById$. Additional work to complete as tech debt tracked in PM-27009. * feat(user-decryption-options) [PM-26413]: Update for non-null strict adherence. * feat(user-decryption-options) [PM-26413]: Update type safety and defensive returns. * chore(user-decryption-options) [PM-26413]: Comment cleanup. * feat(user-decryption-options) [PM-26413]: Update tests. * feat(user-decryption-options) [PM-26413]: Standardize null-checking on active account id for new API consumption. * feat(vault-timeout-settings-service) [PM-26413]: Add test cases to illustrate null active account from AccountService. * fix(fido2-user-verification-service-spec) [PM-26413]: Update test harness to use FakeAccountService. * fix(downstream-components) [PM-26413]: Prefer use of the getUserId operator in all authenticated contexts for user id provided to UserDecryptionOptionsService. --------- Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com> --- .../browser/src/background/main.background.ts | 7 +- .../src/popup/services/services.module.ts | 8 +- .../fido2-user-verification.service.spec.ts | 31 ++++--- .../fido2-user-verification.service.ts | 16 +++- .../service-container/service-container.ts | 5 +- ...sktop-set-initial-password.service.spec.ts | 4 +- .../web-set-initial-password.service.spec.ts | 4 +- .../settings/account/account.component.ts | 10 +- .../password-settings.component.ts | 7 +- .../security/security-keys.component.ts | 22 ++++- .../settings/security/security.component.ts | 14 ++- .../organization-options.component.ts | 5 +- ...initial-password.service.implementation.ts | 7 +- ...fault-set-initial-password.service.spec.ts | 10 +- .../src/services/jslib-services.module.ts | 3 +- .../login-decryption-options.component.ts | 2 +- libs/auth/src/angular/sso/sso.component.ts | 2 +- .../two-factor-auth.component.spec.ts | 4 +- .../two-factor-auth.component.ts | 2 +- ...-decryption-options.service.abstraction.ts | 35 ++++--- .../login-strategies/login.strategy.spec.ts | 3 +- .../common/login-strategies/login.strategy.ts | 3 +- .../sso-login.strategy.spec.ts | 4 +- .../login-strategies/sso-login.strategy.ts | 2 +- .../user-decryption-options.service.spec.ts | 93 ++++++++++--------- .../user-decryption-options.service.ts | 42 ++++----- .../user-verification.service.abstraction.ts | 3 + .../user-verification.service.spec.ts | 23 +---- .../user-verification.service.ts | 19 ++-- .../device-trust.service.implementation.ts | 17 +++- .../services/device-trust.service.spec.ts | 3 +- .../vault-timeout-settings.service.spec.ts | 25 ++++- .../vault-timeout-settings.service.ts | 17 ++-- 33 files changed, 280 insertions(+), 172 deletions(-) diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fecc47af981..78b5e323798 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -728,7 +728,9 @@ export default class MainBackground { this.appIdService = new AppIdService(this.storageService, this.logService); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.userDecryptionOptionsService = new UserDecryptionOptionsService( + this.singleUserStateProvider, + ); this.organizationService = new DefaultOrganizationService(this.stateProvider); this.policyService = new DefaultPolicyService(this.stateProvider, this.organizationService); @@ -859,8 +861,6 @@ export default class MainBackground { this.stateProvider, ); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); - this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, @@ -876,6 +876,7 @@ export default class MainBackground { this.userDecryptionOptionsService, this.logService, this.configService, + this.accountService, ); this.devicesService = new DevicesServiceImplementation( diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index eebf0a08a22..c462319dc2e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -36,6 +36,7 @@ import { LoginEmailService, SsoUrlService, LogoutService, + UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -607,7 +608,12 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: Fido2UserVerificationService, useClass: Fido2UserVerificationService, - deps: [PasswordRepromptService, UserVerificationService, DialogService], + deps: [ + PasswordRepromptService, + UserDecryptionOptionsServiceAbstraction, + DialogService, + AccountServiceAbstraction, + ], }), safeProvider({ provide: AnimationControlService, diff --git a/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts b/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts index 97a22bb2cf3..e55e3091244 100644 --- a/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts +++ b/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts @@ -1,11 +1,15 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; +import { newGuid } from "@bitwarden/guid"; import { PasswordRepromptService } from "@bitwarden/vault"; // FIXME (PM-22628): Popup imports are forbidden in background @@ -31,21 +35,24 @@ describe("Fido2UserVerificationService", () => { let fido2UserVerificationService: Fido2UserVerificationService; let passwordRepromptService: MockProxy; - let userVerificationService: MockProxy; + let userDecryptionOptionsService: MockProxy; let dialogService: MockProxy; + let accountService: FakeAccountService; let cipher: CipherView; beforeEach(() => { passwordRepromptService = mock(); - userVerificationService = mock(); + userDecryptionOptionsService = mock(); dialogService = mock(); + accountService = mockAccountServiceWith(newGuid() as UserId); cipher = createCipherView(); fido2UserVerificationService = new Fido2UserVerificationService( passwordRepromptService, - userVerificationService, + userDecryptionOptionsService, dialogService, + accountService, ); (UserVerificationDialogComponent.open as jest.Mock).mockResolvedValue({ @@ -67,7 +74,7 @@ describe("Fido2UserVerificationService", () => { it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(true); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( @@ -82,7 +89,7 @@ describe("Fido2UserVerificationService", () => { it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(false); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( true, @@ -98,7 +105,7 @@ describe("Fido2UserVerificationService", () => { it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(false); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( true, @@ -114,7 +121,7 @@ describe("Fido2UserVerificationService", () => { it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(false); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( @@ -176,7 +183,7 @@ describe("Fido2UserVerificationService", () => { it("should call master password reprompt dialog if user is redirected from lock screen, has master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(true); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( @@ -191,7 +198,7 @@ describe("Fido2UserVerificationService", () => { it("should call user verification dialog if user is redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(false); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( false, @@ -207,7 +214,7 @@ describe("Fido2UserVerificationService", () => { it("should call user verification dialog if user is not redirected from lock screen, does not have a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(false); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await fido2UserVerificationService.handleUserVerification( false, @@ -223,7 +230,7 @@ describe("Fido2UserVerificationService", () => { it("should call master password reprompt dialog if user is not redirected from lock screen, has a master password and master password reprompt is required", async () => { cipher.reprompt = CipherRepromptType.Password; - userVerificationService.hasMasterPassword.mockResolvedValue(false); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); passwordRepromptService.showPasswordPrompt.mockResolvedValue(true); const result = await fido2UserVerificationService.handleUserVerification( diff --git a/apps/browser/src/vault/services/fido2-user-verification.service.ts b/apps/browser/src/vault/services/fido2-user-verification.service.ts index 9bf9be70fc8..db3951d44d9 100644 --- a/apps/browser/src/vault/services/fido2-user-verification.service.ts +++ b/apps/browser/src/vault/services/fido2-user-verification.service.ts @@ -3,7 +3,8 @@ import { firstValueFrom } from "rxjs"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -15,8 +16,9 @@ import { SetPinComponent } from "../../auth/popup/components/set-pin.component"; export class Fido2UserVerificationService { constructor( private passwordRepromptService: PasswordRepromptService, - private userVerificationService: UserVerificationService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private dialogService: DialogService, + private accountService: AccountService, ) {} /** @@ -78,7 +80,15 @@ export class Fido2UserVerificationService { } private async handleMasterPasswordReprompt(): Promise { - const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + if (!activeAccount?.id) { + return false; + } + + const hasMasterPassword = await firstValueFrom( + this.userDecryptionOptionsService.hasMasterPasswordById$(activeAccount.id), + ); // TDE users have no master password, so we need to use the UserVerification prompt return hasMasterPassword diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 365205a1329..122dd6ea052 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -512,7 +512,9 @@ export class ServiceContainer { ")"; this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.userDecryptionOptionsService = new UserDecryptionOptionsService( + this.singleUserStateProvider, + ); this.ssoUrlService = new SsoUrlService(); this.organizationService = new DefaultOrganizationService(this.stateProvider); @@ -702,6 +704,7 @@ export class ServiceContainer { this.userDecryptionOptionsService, this.logService, this.configService, + this.accountService, ); this.loginStrategyService = new LoginStrategyService( diff --git a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts index 53a1c7dbd4c..717af25a1dc 100644 --- a/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts +++ b/apps/desktop/src/app/services/set-initial-password/desktop-set-initial-password.service.spec.ts @@ -119,7 +119,9 @@ describe("DesktopSetInitialPasswordService", () => { userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true }); userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions); - userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + userDecryptionOptionsSubject, + ); setPasswordRequest = new SetPasswordRequest( credentials.newServerMasterKeyHash, diff --git a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts index 70f7686a2cd..647c9ae83d9 100644 --- a/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts +++ b/apps/web/src/app/auth/core/services/password-management/set-initial-password/web-set-initial-password.service.spec.ts @@ -123,7 +123,9 @@ describe("WebSetInitialPasswordService", () => { userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true }); userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions); - userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + userDecryptionOptionsSubject, + ); setPasswordRequest = new SetPasswordRequest( credentials.newServerMasterKeyHash, diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index 8bae8cd2c1f..3e618b89dbe 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -1,11 +1,10 @@ import { Component, OnInit, OnDestroy } from "@angular/core"; -import { firstValueFrom, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; import { HeaderModule } from "../../../layouts/header/header.module"; @@ -42,8 +41,7 @@ export class AccountComponent implements OnInit, OnDestroy { constructor( private accountService: AccountService, private dialogService: DialogService, - private userVerificationService: UserVerificationService, - private configService: ConfigService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private organizationService: OrganizationService, ) {} @@ -56,7 +54,7 @@ export class AccountComponent implements OnInit, OnDestroy { map((organizations) => organizations.some((o) => o.userIsManagedByOrganization === true)), ); - const hasMasterPassword$ = from(this.userVerificationService.hasMasterPassword()); + const hasMasterPassword$ = this.userDecryptionOptionsService.hasMasterPasswordById$(userId); this.showChangeEmail$ = hasMasterPassword$; diff --git a/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts b/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts index 0e37c856935..ee283d26415 100644 --- a/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts +++ b/apps/web/src/app/auth/settings/security/password-settings/password-settings.component.ts @@ -5,6 +5,8 @@ import { firstValueFrom } from "rxjs"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { InputPasswordFlow } from "@bitwarden/auth/angular"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CalloutModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; @@ -24,12 +26,15 @@ export class PasswordSettingsComponent implements OnInit { constructor( private router: Router, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + private accountService: AccountService, ) {} async ngOnInit() { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const userHasMasterPassword = await firstValueFrom( - this.userDecryptionOptionsService.hasMasterPassword$, + this.userDecryptionOptionsService.hasMasterPasswordById$(userId), ); + if (!userHasMasterPassword) { await this.router.navigate(["/settings/security/two-factor"]); return; diff --git a/apps/web/src/app/auth/settings/security/security-keys.component.ts b/apps/web/src/app/auth/settings/security/security-keys.component.ts index 27a555ff343..b62828a2783 100644 --- a/apps/web/src/app/auth/settings/security/security-keys.component.ts +++ b/apps/web/src/app/auth/settings/security/security-keys.component.ts @@ -1,11 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { firstValueFrom, map } from "rxjs"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DialogService } from "@bitwarden/components"; import { ChangeKdfModule } from "../../../key-management/change-kdf/change-kdf.module"; @@ -23,20 +22,28 @@ export class SecurityKeysComponent implements OnInit { showChangeKdf = true; constructor( - private userVerificationService: UserVerificationService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private accountService: AccountService, private apiService: ApiService, private dialogService: DialogService, ) {} async ngOnInit() { - this.showChangeKdf = await this.userVerificationService.hasMasterPassword(); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.showChangeKdf = await firstValueFrom( + this.userDecryptionOptionsService.hasMasterPasswordById$(userId), + ); } async viewUserApiKey() { const entityId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + + if (!entityId) { + throw new Error("Active account not found"); + } + await ApiKeyComponent.open(this.dialogService, { data: { keyType: "user", @@ -55,6 +62,11 @@ export class SecurityKeysComponent implements OnInit { const entityId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + + if (!entityId) { + throw new Error("Active account not found"); + } + await ApiKeyComponent.open(this.dialogService, { data: { keyType: "user", diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 629de32efc4..85bc29fac63 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from "@angular/core"; -import { Observable } from "rxjs"; +import { firstValueFrom, Observable } from "rxjs"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -20,7 +22,8 @@ export class SecurityComponent implements OnInit { consolidatedSessionTimeoutComponent$: Observable; constructor( - private userVerificationService: UserVerificationService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + private accountService: AccountService, private configService: ConfigService, ) { this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$( @@ -29,6 +32,9 @@ export class SecurityComponent implements OnInit { } async ngOnInit() { - this.showChangePassword = await this.userVerificationService.hasMasterPassword(); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.showChangePassword = userId + ? await firstValueFrom(this.userDecryptionOptionsService.hasMasterPasswordById$(userId)) + : false; } } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index 3b707f2d78c..37b881406e3 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -95,7 +95,10 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { combineLatest([ this.organization$, resetPasswordPolicies$, - this.userDecryptionOptionsService.userDecryptionOptions$, + this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.userDecryptionOptionsService.userDecryptionOptionsById$(userId)), + ), managingOrg$, ]) .pipe(takeUntil(this.destroy$)) diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts index dcd4fd93cba..df5220b5255 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.implementation.ts @@ -198,10 +198,13 @@ export class DefaultSetInitialPasswordService implements SetInitialPasswordServi userId: UserId, ) { const userDecryptionOpts = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptions$, + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), ); userDecryptionOpts.hasMasterPassword = true; - await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); + await this.userDecryptionOptionsService.setUserDecryptionOptionsById( + userId, + userDecryptionOpts, + ); await this.kdfConfigService.setKdfConfig(userId, kdfConfig); await this.masterPasswordService.setMasterKey(masterKey, userId); await this.keyService.setUserKey(masterKeyEncryptedUserKey[0], userId); diff --git a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts index 7a1dfc91e67..8b95090e776 100644 --- a/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts +++ b/libs/angular/src/auth/password-management/set-initial-password/default-set-initial-password.service.spec.ts @@ -149,7 +149,9 @@ describe("DefaultSetInitialPasswordService", () => { userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true }); userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions); - userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + userDecryptionOptionsSubject, + ); setPasswordRequest = new SetPasswordRequest( credentials.newServerMasterKeyHash, @@ -362,7 +364,8 @@ describe("DefaultSetInitialPasswordService", () => { // Assert expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); - expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith( + userId, userDecryptionOptions, ); expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); @@ -560,7 +563,8 @@ describe("DefaultSetInitialPasswordService", () => { // Assert expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest); - expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith( + userId, userDecryptionOptions, ); expect(kdfConfigService.setKdfConfig).toHaveBeenCalledWith(userId, credentials.kdfConfig); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index ff5caff540c..bcb601a993c 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -684,7 +684,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: InternalUserDecryptionOptionsServiceAbstraction, useClass: UserDecryptionOptionsService, - deps: [StateProvider], + deps: [SingleUserStateProvider], }), safeProvider({ provide: UserDecryptionOptionsServiceAbstraction, @@ -1292,6 +1292,7 @@ const safeProviders: SafeProvider[] = [ UserDecryptionOptionsServiceAbstraction, LogService, ConfigService, + AccountServiceAbstraction, ], }), safeProvider({ diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 26293285008..fb07069998b 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -135,7 +135,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { try { const userDecryptionOptions = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptions$, + this.userDecryptionOptionsService.userDecryptionOptionsById$(this.activeAccountId), ); if ( diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index 0b6bb1159f4..bf618ba39f4 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -460,7 +460,7 @@ export class SsoComponent implements OnInit { // must come after 2fa check since user decryption options aren't available if 2fa is required const userDecryptionOpts = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptions$, + this.userDecryptionOptionsService.userDecryptionOptionsById$(authResult.userId), ); const tdeEnabled = userDecryptionOpts.trustedDeviceOption diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts index af9fb03e01e..8c12060168b 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.spec.ts @@ -176,7 +176,9 @@ describe("TwoFactorAuthComponent", () => { selectedUserDecryptionOptions = new BehaviorSubject( mockUserDecryptionOpts.withMasterPassword, ); - mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; + mockUserDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + selectedUserDecryptionOptions, + ); TestBed.configureTestingModule({ declarations: [TestTwoFactorComponent], diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 8e10539823d..ca19d3652bb 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -473,7 +473,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { } const userDecryptionOpts = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptions$, + this.userDecryptionOptionsService.userDecryptionOptionsById$(authResult.userId), ); const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption); diff --git a/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts b/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts index e46fb09cff6..443e43b3151 100644 --- a/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts @@ -1,34 +1,45 @@ import { Observable } from "rxjs"; +import { UserId } from "@bitwarden/common/types/guid"; + import { UserDecryptionOptions } from "../models"; +/** + * Public service for reading user decryption options. + * For use in components and services that need to evaluate user decryption settings. + */ export abstract class UserDecryptionOptionsServiceAbstraction { /** - * Returns what decryption options are available for the current user. - * @remark This is sent from the server on authentication. + * Returns the user decryption options for the given user id. + * Will only emit when options are set (does not emit null/undefined + * for an unpopulated state), and should not be called in an unauthenticated context. + * @param userId The user id to check. */ - abstract userDecryptionOptions$: Observable; + abstract userDecryptionOptionsById$(userId: UserId): Observable; /** * Uses user decryption options to determine if current user has a master password. * @remark This is sent from the server, and does not indicate if the master password * was used to login and/or if a master key is saved locally. */ - abstract hasMasterPassword$: Observable; - - /** - * Returns the user decryption options for the given user id. - * @param userId The user id to check. - */ - abstract userDecryptionOptionsById$(userId: string): Observable; + abstract hasMasterPasswordById$(userId: UserId): Observable; } +/** + * Internal service for managing user decryption options. + * For use only in authentication flows that need to update decryption options + * (e.g., login strategies). Extends consumer methods from {@link UserDecryptionOptionsServiceAbstraction}. + * @remarks Most consumers should use UserDecryptionOptionsServiceAbstraction instead. + */ export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction { /** - * Sets the current decryption options for the user, contains the current configuration + * Sets the current decryption options for the user. Contains the current configuration * of the users account related to how they can decrypt their vault. * @remark Intended to be used when user decryption options are received from server, does * not update the server. Consider syncing instead of updating locally. * @param userDecryptionOptions Current user decryption options received from server. */ - abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise; + abstract setUserDecryptionOptionsById( + userId: UserId, + userDecryptionOptions: UserDecryptionOptions, + ): Promise; } diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index e9eed27d5a1..38d62cfdd83 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -257,7 +257,8 @@ describe("LoginStrategy", () => { expect(environmentService.seedUserEnvironment).toHaveBeenCalled(); - expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + expect(userDecryptionOptionsService.setUserDecryptionOptionsById).toHaveBeenCalledWith( + userId, UserDecryptionOptions.fromResponse(idTokenResponse), ); expect(masterPasswordService.mock.setMasterPasswordUnlockData).toHaveBeenCalledWith( diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 35f13246593..b8e4ee9e822 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -195,7 +195,8 @@ export abstract class LoginStrategy { // We must set user decryption options before retrieving vault timeout settings // as the user decryption options help determine the available timeout actions. - await this.userDecryptionOptionsService.setUserDecryptionOptions( + await this.userDecryptionOptionsService.setUserDecryptionOptionsById( + userId, UserDecryptionOptions.fromResponse(tokenResponse), ); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index 03de5f36c2d..acbb680ae6d 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -134,7 +134,9 @@ describe("SsoLoginStrategy", () => { ); const userDecryptionOptions = new UserDecryptionOptions(); - userDecryptionOptionsService.userDecryptionOptions$ = of(userDecryptionOptions); + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); ssoLoginStrategy = new SsoLoginStrategy( {} as SsoLoginStrategyData, diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index ec7914b087e..d806f6d733e 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -393,7 +393,7 @@ export class SsoLoginStrategy extends LoginStrategy { // Check for TDE-related conditions const userDecryptionOptions = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptions$, + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), ); if (!userDecryptionOptions) { diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts index c2fafc1a2f6..efb00b9aa63 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts @@ -1,12 +1,8 @@ import { firstValueFrom } from "rxjs"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { - FakeAccountService, - FakeStateProvider, - mockAccountServiceWith, -} from "@bitwarden/common/spec"; +import { FakeSingleUserStateProvider } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { newGuid } from "@bitwarden/guid"; import { UserDecryptionOptions } from "../../models/domain/user-decryption-options"; @@ -17,15 +13,10 @@ import { describe("UserDecryptionOptionsService", () => { let sut: UserDecryptionOptionsService; - - const fakeUserId = Utils.newGuid() as UserId; - let fakeAccountService: FakeAccountService; - let fakeStateProvider: FakeStateProvider; + let fakeStateProvider: FakeSingleUserStateProvider; beforeEach(() => { - fakeAccountService = mockAccountServiceWith(fakeUserId); - fakeStateProvider = new FakeStateProvider(fakeAccountService); - + fakeStateProvider = new FakeSingleUserStateProvider(); sut = new UserDecryptionOptionsService(fakeStateProvider); }); @@ -42,55 +33,71 @@ describe("UserDecryptionOptionsService", () => { }, }; - describe("userDecryptionOptions$", () => { - it("should return the active user's decryption options", async () => { - await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions); + describe("userDecryptionOptionsById$", () => { + it("should return user decryption options for a specific user", async () => { + const userId = newGuid() as UserId; - const result = await firstValueFrom(sut.userDecryptionOptions$); + fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS).nextState(userDecryptionOptions); + + const result = await firstValueFrom(sut.userDecryptionOptionsById$(userId)); expect(result).toEqual(userDecryptionOptions); }); }); - describe("hasMasterPassword$", () => { - it("should return the hasMasterPassword property of the active user's decryption options", async () => { - await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions); + describe("hasMasterPasswordById$", () => { + it("should return true when user has a master password", async () => { + const userId = newGuid() as UserId; - const result = await firstValueFrom(sut.hasMasterPassword$); + fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS).nextState(userDecryptionOptions); + + const result = await firstValueFrom(sut.hasMasterPasswordById$(userId)); expect(result).toBe(true); }); - }); - describe("userDecryptionOptionsById$", () => { - it("should return the user decryption options for the given user", async () => { - const givenUser = Utils.newGuid() as UserId; - await fakeAccountService.addAccount(givenUser, { - name: "Test User 1", - email: "test1@email.com", - emailVerified: false, - }); - await fakeStateProvider.setUserState( - USER_DECRYPTION_OPTIONS, - userDecryptionOptions, - givenUser, - ); + it("should return false when user does not have a master password", async () => { + const userId = newGuid() as UserId; + const optionsWithoutMasterPassword = { + ...userDecryptionOptions, + hasMasterPassword: false, + }; - const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser)); + fakeStateProvider + .getFake(userId, USER_DECRYPTION_OPTIONS) + .nextState(optionsWithoutMasterPassword); - expect(result).toEqual(userDecryptionOptions); + const result = await firstValueFrom(sut.hasMasterPasswordById$(userId)); + + expect(result).toBe(false); }); }); - describe("setUserDecryptionOptions", () => { - it("should set the active user's decryption options", async () => { - await sut.setUserDecryptionOptions(userDecryptionOptions); + describe("setUserDecryptionOptionsById", () => { + it("should set user decryption options for a specific user", async () => { + const userId = newGuid() as UserId; - const result = await firstValueFrom( - fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$, - ); + await sut.setUserDecryptionOptionsById(userId, userDecryptionOptions); + + const fakeState = fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS); + const result = await firstValueFrom(fakeState.state$); expect(result).toEqual(userDecryptionOptions); }); + + it("should overwrite existing user decryption options", async () => { + const userId = newGuid() as UserId; + const initialOptions = { ...userDecryptionOptions, hasMasterPassword: false }; + const updatedOptions = { ...userDecryptionOptions, hasMasterPassword: true }; + + const fakeState = fakeStateProvider.getFake(userId, USER_DECRYPTION_OPTIONS); + fakeState.nextState(initialOptions); + + await sut.setUserDecryptionOptionsById(userId, updatedOptions); + + const result = await firstValueFrom(fakeState.state$); + + expect(result).toEqual(updatedOptions); + }); }); }); diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts index 7c44a6f1682..a0075d1987b 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts @@ -1,16 +1,11 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Observable, map } from "rxjs"; +import { Observable, filter, map } from "rxjs"; import { - ActiveUserState, - StateProvider, + SingleUserStateProvider, USER_DECRYPTION_OPTIONS_DISK, UserKeyDefinition, } from "@bitwarden/common/platform/state"; -// FIXME: remove `src` and fix import -// eslint-disable-next-line no-restricted-imports -import { UserId } from "@bitwarden/common/src/types/guid"; +import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; import { UserDecryptionOptions } from "../../models"; @@ -27,25 +22,26 @@ export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition; + constructor(private singleUserStateProvider: SingleUserStateProvider) {} - userDecryptionOptions$: Observable; - hasMasterPassword$: Observable; + userDecryptionOptionsById$(userId: UserId): Observable { + return this.singleUserStateProvider + .get(userId, USER_DECRYPTION_OPTIONS) + .state$.pipe(filter((options): options is UserDecryptionOptions => options != null)); + } - constructor(private stateProvider: StateProvider) { - this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS); - - this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$; - this.hasMasterPassword$ = this.userDecryptionOptions$.pipe( - map((options) => options?.hasMasterPassword ?? false), + hasMasterPasswordById$(userId: UserId): Observable { + return this.userDecryptionOptionsById$(userId).pipe( + map((options) => options.hasMasterPassword ?? false), ); } - userDecryptionOptionsById$(userId: UserId): Observable { - return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$; - } - - async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise { - await this.userDecryptionOptionsState.update((_) => userDecryptionOptions); + async setUserDecryptionOptionsById( + userId: UserId, + userDecryptionOptions: UserDecryptionOptions, + ): Promise { + await this.singleUserStateProvider + .get(userId, USER_DECRYPTION_OPTIONS) + .update((_) => userDecryptionOptions); } } diff --git a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts index d9749d9467c..b9bc9108e52 100644 --- a/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/user-verification/user-verification.service.abstraction.ts @@ -48,6 +48,9 @@ export abstract class UserVerificationService { * @param userId The user id to check. If not provided, the current user is used * @returns True if the user has a master password * @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead + * @remark To facilitate deprecation, many call sites were removed as part of PM-26413. + * Those remaining are blocked by currently-disallowed imports of auth/common. + * PM-27009 has been filed to track completion of this deprecation. */ abstract hasMasterPassword(userId?: string): Promise; /** diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index e7fbc002af8..e570c0f4a43 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -3,10 +3,7 @@ import { of } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports -import { - UserDecryptionOptions, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { @@ -146,11 +143,7 @@ describe("UserVerificationService", () => { describe("server verification type", () => { it("correctly returns master password availability", async () => { - userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( - of({ - hasMasterPassword: true, - } as UserDecryptionOptions), - ); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(true)); const result = await sut.getAvailableVerificationOptions("server"); @@ -168,11 +161,7 @@ describe("UserVerificationService", () => { }); it("correctly returns OTP availability", async () => { - userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( - of({ - hasMasterPassword: false, - } as UserDecryptionOptions), - ); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(false)); const result = await sut.getAvailableVerificationOptions("server"); @@ -526,11 +515,7 @@ describe("UserVerificationService", () => { // Helpers function setMasterPasswordAvailability(hasMasterPassword: boolean) { - userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( - of({ - hasMasterPassword: hasMasterPassword, - } as UserDecryptionOptions), - ); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue(of(hasMasterPassword)); masterPasswordService.masterKeyHash$.mockReturnValue( of(hasMasterPassword ? "masterKeyHash" : null), ); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 7a537b883e3..7d93120148b 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -258,16 +258,19 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } async hasMasterPassword(userId?: string): Promise { - if (userId) { - const decryptionOptions = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), - ); + const resolvedUserId = userId ?? (await firstValueFrom(this.accountService.activeAccount$))?.id; - if (decryptionOptions?.hasMasterPassword != undefined) { - return decryptionOptions.hasMasterPassword; - } + if (!resolvedUserId) { + return false; } - return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); + + // Ideally, this method would accept a UserId over string. To avoid scope creep in PM-26413, we are + // doing the cast here. Future work should be done to make this type-safe, and should be considered + // as part of PM-27009. + + return await firstValueFrom( + this.userDecryptionOptionsService.hasMasterPasswordById$(resolvedUserId as UserId), + ); } async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise { diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts index aa14c7f0c4f..59bd7bc11f2 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.implementation.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map, Observable, Subject } from "rxjs"; +import { firstValueFrom, map, Observable, Subject, switchMap } from "rxjs"; // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports @@ -9,6 +9,7 @@ import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common" // eslint-disable-next-line no-restricted-imports import { KeyService } from "@bitwarden/key-management"; +import { AccountService } from "../../../auth/abstractions/account.service"; import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response"; import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction"; import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; @@ -87,10 +88,18 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private logService: LogService, private configService: ConfigService, + private accountService: AccountService, ) { - this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( - map((options) => { - return options?.trustedDeviceOption != null; + this.supportsDeviceTrust$ = this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account == null) { + return [false]; + } + return this.userDecryptionOptionsService.userDecryptionOptionsById$(account.id).pipe( + map((options) => { + return options?.trustedDeviceOption != null; + }), + ); }), ); } diff --git a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts index e735295f42b..024a21766ee 100644 --- a/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts +++ b/libs/common/src/key-management/device-trust/services/device-trust.service.spec.ts @@ -914,7 +914,7 @@ describe("deviceTrustService", () => { platformUtilsService.supportsSecureStorage.mockReturnValue(supportsSecureStorage); decryptionOptions.next({} as any); - userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(decryptionOptions); return new DeviceTrustService( keyGenerationService, @@ -930,6 +930,7 @@ describe("deviceTrustService", () => { userDecryptionOptionsService, logService, configService, + accountService, ); } }); diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts index f3fec6d849a..ba58fa80922 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.spec.ts @@ -53,9 +53,11 @@ describe("VaultTimeoutSettingsService", () => { policyService = mock(); userDecryptionOptionsSubject = new BehaviorSubject(null); - userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; - userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe( - map((options) => options?.hasMasterPassword ?? false), + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + userDecryptionOptionsSubject, + ); + userDecryptionOptionsService.hasMasterPasswordById$.mockReturnValue( + userDecryptionOptionsSubject.pipe(map((options) => options?.hasMasterPassword ?? false)), ); userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( userDecryptionOptionsSubject, @@ -127,6 +129,23 @@ describe("VaultTimeoutSettingsService", () => { expect(result).not.toContain(VaultTimeoutAction.Lock); }); + + it("should return only LogOut when userId is not provided and there is no active account", async () => { + // Set up accountService to return null for activeAccount + accountService.activeAccount$ = of(null); + pinStateService.isPinSet.mockResolvedValue(false); + biometricStateService.biometricUnlockEnabled$ = of(false); + + // Call availableVaultTimeoutActions$ which internally calls userHasMasterPassword without a userId + const result = await firstValueFrom( + vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + ); + + // Since there's no active account, userHasMasterPassword returns false, + // meaning no master password is available, so Lock should not be available + expect(result).toEqual([VaultTimeoutAction.LogOut]); + expect(result).not.toContain(VaultTimeoutAction.Lock); + }); }); describe("canLock", () => { diff --git a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts index 4ca23bb24bf..00e53596de4 100644 --- a/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts +++ b/libs/common/src/key-management/vault-timeout/services/vault-timeout-settings.service.ts @@ -290,14 +290,19 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } private async userHasMasterPassword(userId: string): Promise { + let resolvedUserId: UserId; if (userId) { - const decryptionOptions = await firstValueFrom( - this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), - ); - - return !!decryptionOptions?.hasMasterPassword; + resolvedUserId = userId as UserId; } else { - return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (!activeAccount) { + return false; // No account, can't have master password + } + resolvedUserId = activeAccount.id; } + + return await firstValueFrom( + this.userDecryptionOptionsService.hasMasterPasswordById$(resolvedUserId), + ); } } From 17ae78ea8315e5b266d7ff9af393c61848859825 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:23:01 -0600 Subject: [PATCH 25/44] chore: fix feature flag name, refs PM-27766 (#17660) --- libs/common/src/enums/feature-flag.enum.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 17d5f4e9df5..56a25cb213c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -13,7 +13,7 @@ export enum FeatureFlag { /* Admin Console Team */ CreateDefaultLocation = "pm-19467-create-default-location", AutoConfirm = "pm-19934-auto-confirm-organization-users", - BlockClaimedDomainAccountCreation = "block-claimed-domain-account-creation", + BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration", /* Auth */ PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin", From 441783627b9af11a0ca339dcc7aa789787b3c468 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:28:34 -0600 Subject: [PATCH 26/44] [PM-26359] Archive Upgrade - Browser (#16904) * add archive upgrade flow to more options menu * add reprompt for archiving a cipher * add premium badge for archive in settings * update showArchive to only look at the feature flag * add premium badge for browser settings * add event to prompt for premium * formatting * update test --- apps/browser/src/_locales/en/messages.json | 3 ++ .../item-more-options.component.html | 24 +++++++++-- .../item-more-options.component.spec.ts | 5 ++- .../item-more-options.component.ts | 40 ++++++++++++------- .../settings/vault-settings-v2.component.html | 28 +++++++++---- .../settings/vault-settings-v2.component.ts | 21 +++++++--- 6 files changed, 90 insertions(+), 31 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 14915175da1..6009bcba1bd 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -585,6 +585,9 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "upgradeToUseArchive": { + "message": "A premium membership is required to use Archive." + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index b6a7002139c..5c5171ac81d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -51,10 +51,26 @@ {{ "assignToCollections" | i18n }} - @if (canArchive$ | async) { - + @if (showArchive$ | async) { + @if (canArchive$ | async) { + + } @else { + + } } @if (canDelete$ | async) { + + + + + Foo + Bar + + + `, + }), +}; + +export const Basic: Story = { + render: (args: any) => ({ + props: args, + template: /*html*/ ` + + `, + }), +}; + +export const WithLongTitle: Story = { + render: (arg: any) => ({ + props: arg, + template: /*html*/ ` + + + + `, + }), +}; + +export const WithBreadcrumbs: Story = { + render: (args: any) => ({ + props: args, + template: /*html*/ ` + + + Foo + Bar + + + `, + }), +}; + +export const WithSearch: Story = { + render: (args: any) => ({ + props: args, + template: /*html*/ ` + + + + `, + }), +}; + +export const WithSecondaryContent: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + `, + }), +}; + +export const WithTabs: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + Foo + Bar + + + `, + }), +}; + +export const WithTitleSuffixComponent: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + `, + }), +}; diff --git a/libs/components/src/header/index.ts b/libs/components/src/header/index.ts new file mode 100644 index 00000000000..c2d38d375ed --- /dev/null +++ b/libs/components/src/header/index.ts @@ -0,0 +1 @@ +export * from "./header.component"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 5346747d3b2..410a96f1cb3 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -19,6 +19,7 @@ export * from "./dialog"; export * from "./disclosure"; export * from "./drawer"; export * from "./form-field"; +export * from "./header"; export * from "./icon-button"; export * from "./icon"; export * from "./icon-tile"; From eae894123dfe275371b565591a9c6f1e53715b5d Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:33:21 -0800 Subject: [PATCH 28/44] [PM-28376] - update copy for autofill confirmation dialog url list expand button (#17594) * update copy for autofill confirmation dialog url list expand button * fix tests --- apps/browser/src/_locales/en/messages.json | 3 ++ ...utofill-confirmation-dialog.component.html | 2 +- ...fill-confirmation-dialog.component.spec.ts | 53 +++++++++++++++---- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 6009bcba1bd..fe979e129f4 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -597,6 +597,9 @@ "viewAll": { "message": "View all" }, + "showAll": { + "message": "Show all" + }, "viewLess": { "message": "View less" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html index 6f61c5fa446..88bff47191a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.html @@ -24,7 +24,7 @@ class="tw-text-sm tw-font-medium tw-cursor-pointer" (click)="toggleSavedUrlExpandedState()" > - {{ (savedUrlsExpanded() ? "viewLess" : "viewAll") | i18n }} + {{ (savedUrlsExpanded() ? "showLess" : "showAll") | i18n }}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts index e40019d99b6..a28b8730109 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-confirmation-dialog/autofill-confirmation-dialog.component.spec.ts @@ -91,6 +91,11 @@ describe("AutofillConfirmationDialogComponent", () => { jest.resetAllMocks(); }); + const findShowAll = (inFx?: ComponentFixture) => + (inFx || fixture).nativeElement.querySelector( + "button.tw-text-sm.tw-font-medium.tw-cursor-pointer", + ) as HTMLButtonElement | null; + it("normalizes currentUrl and savedUrls via Utils.getHostname", () => { expect(Utils.getHostname).toHaveBeenCalledTimes(1 + (params.savedUrls?.length ?? 0)); expect(component.currentUrl()).toBe("example.com"); @@ -191,21 +196,47 @@ describe("AutofillConfirmationDialogComponent", () => { expect(text).toContain("two.example.com"); }); - it("shows the 'view all' button when savedUrls > 1 and toggles the button text when clicked", () => { - const findViewAll = () => - fixture.nativeElement.querySelector( - "button.tw-text-sm.tw-font-medium.tw-cursor-pointer", - ) as HTMLButtonElement | null; - - let btn = findViewAll(); + it("shows the 'show all' button when savedUrls > 1", () => { + const btn = findShowAll(); expect(btn).toBeTruthy(); + expect(btn!.textContent).toContain("showAll"); + }); + it('hides the "show all" button when savedUrls is empty', async () => { + const newParams: AutofillConfirmationDialogParams = { + currentUrl: "https://bitwarden.com/help", + savedUrls: [], + }; + + const { fixture: vf } = await createFreshFixture({ params: newParams }); + vf.detectChanges(); + const btn = findShowAll(vf); + expect(btn).toBeNull(); + }); + + it("handles toggling of the 'show all' button correctly", async () => { + const { fixture: vf, component: vc } = await createFreshFixture(); + + let btn = findShowAll(vf); + expect(btn).toBeTruthy(); + expect(vc.savedUrlsExpanded()).toBe(false); + expect(btn!.textContent).toContain("showAll"); + + // click to expand btn!.click(); - fixture.detectChanges(); + vf.detectChanges(); - btn = findViewAll(); - expect(btn!.textContent).toContain("viewLess"); - expect(component.savedUrlsExpanded()).toBe(true); + btn = findShowAll(vf); + expect(btn!.textContent).toContain("showLess"); + expect(vc.savedUrlsExpanded()).toBe(true); + + // click to collapse + btn!.click(); + vf.detectChanges(); + + btn = findShowAll(vf); + expect(btn!.textContent).toContain("showAll"); + expect(vc.savedUrlsExpanded()).toBe(false); }); it("shows autofillWithoutAdding text on autofill button when viewOnly is false", () => { From 3de3bee08ff8d77b0333997b7c60202accd824f4 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Tue, 25 Nov 2025 13:42:46 -0500 Subject: [PATCH 29/44] [PM-27821]Add validation of extension origin for uses of window.postMessage (#17476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * PM-27821 - Replace chrome.runtime.getURL() with BrowserApi.getRuntimeURL() for consistency - Add extension origin validation for all window.postMessage calls - Implement token-based authentication for inline menu communications - Add message source validation (event.source === globalThis.parent) - Add command presence validation (- Update notification bar to validate message origins and commands - Add extensionOrigin property to services using postMessage - Generate session tokens for inline menu containers (32-char random) - Validate tokens in message handlers to prevent unauthorized commands * Add explicit token validation * only set when receiving the trusted initNotificationBar message * await windowmessageorigin before posting to parent * fix tests * the parent must include its origin in the message for notification bar race condition * reduce if statements to one block and comment * extract parentOrigin from the URL and set windoMessageOrigin accordingly * consolidate if statements * add bar.spec file * fix merge conflict --- .../background/notification.background.ts | 2 +- .../autofill/background/overlay.background.ts | 6 +- .../content/content-message-handler.spec.ts | 14 +- .../abstractions/notification-bar.ts | 1 + .../src/autofill/notification/bar.spec.ts | 121 ++++++++++++++++ apps/browser/src/autofill/notification/bar.ts | 34 ++++- .../autofill-inline-menu-button.ts | 1 + .../autofill-inline-menu-container.ts | 2 +- .../abstractions/autofill-inline-menu-list.ts | 1 + ...utofill-inline-menu-iframe.service.spec.ts | 12 +- .../autofill-inline-menu-iframe.service.ts | 10 +- .../autofill-inline-menu-button.spec.ts | 21 +-- .../list/autofill-inline-menu-list.spec.ts | 129 ++++++++++++------ .../autofill-inline-menu-container.ts | 11 +- .../autofill-inline-menu-page-element.ts | 42 ++++-- ...notifications-content.service.spec.ts.snap | 2 +- ...rlay-notifications-content.service.spec.ts | 5 +- .../overlay-notifications-content.service.ts | 16 ++- .../src/autofill/spec/autofill-mocks.ts | 2 + .../src/autofill/spec/testing-utils.ts | 8 +- 20 files changed, 344 insertions(+), 96 deletions(-) create mode 100644 apps/browser/src/autofill/notification/bar.spec.ts diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index de1514f0342..547c5ba1575 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1344,7 +1344,7 @@ export default class NotificationBackground { return; } - const extensionUrl = chrome.runtime.getURL("popup/index.html"); + const extensionUrl = BrowserApi.getRuntimeURL("popup/index.html"); const unlockPopoutTabs = (await BrowserApi.tabsQuery({ url: `${extensionUrl}*` })).filter( (tab) => tab.url?.includes(`singleActionPopout=${AuthPopoutType.unlockExtension}`), ); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 0eb7d070de3..af8141f1ab8 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -2949,13 +2949,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { (await this.checkFocusedFieldHasValue(port.sender.tab)) && (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)); - const iframeUrl = chrome.runtime.getURL( + const iframeUrl = BrowserApi.getRuntimeURL( `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, ); - const styleSheetUrl = chrome.runtime.getURL( + const styleSheetUrl = BrowserApi.getRuntimeURL( `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, ); - const extensionOrigin = new URL(iframeUrl).origin; + const extensionOrigin = iframeUrl ? new URL(iframeUrl).origin : null; this.postMessageToPort(port, { command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts index fe023f344d6..874e1cc76ff 100644 --- a/apps/browser/src/autofill/content/content-message-handler.spec.ts +++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts @@ -56,7 +56,11 @@ describe("ContentMessageHandler", () => { }); it("sends an authResult message", () => { - postWindowMessage({ command: "authResult", lastpass: true, code: "code", state: "state" }); + postWindowMessage( + { command: "authResult", lastpass: true, code: "code", state: "state" }, + "https://localhost/", + window, + ); expect(sendMessageSpy).toHaveBeenCalledWith({ command: "authResult", @@ -68,7 +72,11 @@ describe("ContentMessageHandler", () => { }); it("sends a webAuthnResult message", () => { - postWindowMessage({ command: "webAuthnResult", data: "data", remember: true }); + postWindowMessage( + { command: "webAuthnResult", data: "data", remember: true }, + "https://localhost/", + window, + ); expect(sendMessageSpy).toHaveBeenCalledWith({ command: "webAuthnResult", @@ -82,7 +90,7 @@ describe("ContentMessageHandler", () => { const mockCode = "mockCode"; const command = "duoResult"; - postWindowMessage({ command: command, code: mockCode }); + postWindowMessage({ command: command, code: mockCode }, "https://localhost/", window); expect(sendMessageSpy).toHaveBeenCalledWith({ command: command, diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 7881d2f1cac..b23c3c17abb 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -51,6 +51,7 @@ type NotificationBarWindowMessage = { }; error?: string; initData?: NotificationBarIframeInitData; + parentOrigin?: string; }; type NotificationBarWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/notification/bar.spec.ts b/apps/browser/src/autofill/notification/bar.spec.ts new file mode 100644 index 00000000000..ae60e2efc91 --- /dev/null +++ b/apps/browser/src/autofill/notification/bar.spec.ts @@ -0,0 +1,121 @@ +import { mock } from "jest-mock-extended"; + +import { postWindowMessage } from "../spec/testing-utils"; + +import { NotificationBarWindowMessage } from "./abstractions/notification-bar"; +import "./bar"; + +jest.mock("lit", () => ({ render: jest.fn() })); +jest.mock("@lit-labs/signals", () => ({ + signal: jest.fn((testValue) => ({ get: (): typeof testValue => testValue })), +})); +jest.mock("../content/components/notification/container", () => ({ + NotificationContainer: jest.fn(), +})); + +describe("NotificationBar iframe handleWindowMessage security", () => { + const trustedOrigin = "http://localhost"; + const maliciousOrigin = "https://malicious.com"; + + const createMessage = ( + overrides: Partial = {}, + ): NotificationBarWindowMessage => ({ + command: "initNotificationBar", + ...overrides, + }); + + beforeEach(() => { + Object.defineProperty(globalThis, "location", { + value: { search: `?parentOrigin=${encodeURIComponent(trustedOrigin)}` }, + writable: true, + configurable: true, + }); + Object.defineProperty(globalThis, "parent", { + value: mock(), + writable: true, + configurable: true, + }); + globalThis.dispatchEvent(new Event("load")); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it.each([ + { + description: "not from parent window", + message: () => createMessage(), + origin: trustedOrigin, + source: () => mock(), + }, + { + description: "with mismatched origin", + message: () => createMessage(), + origin: maliciousOrigin, + source: () => globalThis.parent, + }, + { + description: "without command field", + message: () => ({}), + origin: trustedOrigin, + source: () => globalThis.parent, + }, + { + description: "initNotificationBar with mismatched parentOrigin", + message: () => createMessage({ parentOrigin: maliciousOrigin }), + origin: trustedOrigin, + source: () => globalThis.parent, + }, + { + description: "when windowMessageOrigin is not set", + message: () => createMessage(), + origin: "different-origin", + source: () => globalThis.parent, + resetOrigin: true, + }, + { + description: "with null source", + message: () => createMessage(), + origin: trustedOrigin, + source: (): null => null, + }, + { + description: "with unknown command", + message: () => createMessage({ command: "unknownCommand" }), + origin: trustedOrigin, + source: () => globalThis.parent, + }, + ])("should reject messages $description", ({ message, origin, source, resetOrigin }) => { + if (resetOrigin) { + Object.defineProperty(globalThis, "location", { + value: { search: "" }, + writable: true, + configurable: true, + }); + } + const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation(); + postWindowMessage(message(), origin, source()); + expect(spy).not.toHaveBeenCalled(); + }); + + it("should accept and handle valid trusted messages", () => { + const spy = jest.spyOn(globalThis.parent, "postMessage").mockImplementation(); + spy.mockClear(); + + const validMessage = createMessage({ + parentOrigin: trustedOrigin, + initData: { + type: "change", + isVaultLocked: false, + removeIndividualVault: false, + importType: null, + launchTimestamp: Date.now(), + }, + }); + postWindowMessage(validMessage, trustedOrigin, globalThis.parent); + expect(validMessage.command).toBe("initNotificationBar"); + expect(validMessage.parentOrigin).toBe(trustedOrigin); + expect(validMessage.initData).toBeDefined(); + }); +}); diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 3673a9f7321..333f8d5e534 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -24,6 +24,13 @@ import { let notificationBarIframeInitData: NotificationBarIframeInitData = {}; let windowMessageOrigin: string; +const urlParams = new URLSearchParams(globalThis.location.search); +const trustedParentOrigin = urlParams.get("parentOrigin"); + +if (trustedParentOrigin) { + windowMessageOrigin = trustedParentOrigin; +} + const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = { initNotificationBar: ({ message }) => initNotificationBar(message), saveCipherAttemptCompleted: ({ message }) => handleSaveCipherConfirmation(message), @@ -395,15 +402,27 @@ function setupWindowMessageListener() { } function handleWindowMessage(event: MessageEvent) { - if (!windowMessageOrigin) { - windowMessageOrigin = event.origin; - } - - if (event.origin !== windowMessageOrigin) { + if (event?.source !== globalThis.parent) { return; } const message = event.data as NotificationBarWindowMessage; + if (!message?.command) { + return; + } + + if (!windowMessageOrigin || event.origin !== windowMessageOrigin) { + return; + } + + if ( + message.command === "initNotificationBar" && + message.parentOrigin && + message.parentOrigin !== event.origin + ) { + return; + } + const handler = notificationBarWindowMessageHandlers[message.command]; if (!handler) { return; @@ -431,5 +450,8 @@ function getResolvedTheme(theme: Theme) { } function postMessageToParent(message: NotificationBarWindowMessage) { - globalThis.parent.postMessage(message, windowMessageOrigin || "*"); + if (!windowMessageOrigin) { + return; + } + globalThis.parent.postMessage(message, windowMessageOrigin); } diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-button.ts index 642e7dd24e9..0836ecf5ff1 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-button.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-button.ts @@ -10,6 +10,7 @@ export type InitAutofillInlineMenuButtonMessage = UpdateAuthStatusMessage & { styleSheetUrl: string; translations: Record; portKey: string; + token: string; }; export type AutofillInlineMenuButtonWindowMessageHandlers = { diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts index 64fa8dde124..98fd84373a8 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-container.ts @@ -5,7 +5,7 @@ import { InlineMenuCipherData } from "../../../background/abstractions/overlay.b export type AutofillInlineMenuContainerMessage = { command: string; portKey: string; - token?: string; + token: string; }; export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMessage & { diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts index f5e1fe08850..cf778ef7892 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts @@ -27,6 +27,7 @@ export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage & showInlineMenuAccountCreation?: boolean; showPasskeysLabels?: boolean; portKey: string; + token: string; generatedPassword?: string; showSaveLoginMenu?: boolean; }; diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts index 9f2947c2e99..3bb86ee7876 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.spec.ts @@ -191,7 +191,7 @@ describe("AutofillInlineMenuIframeService", () => { expect( autofillInlineMenuIframeService["iframe"].contentWindow.postMessage, - ).toHaveBeenCalledWith(message, "*"); + ).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]); }); it("handles port messages that are registered with the message handlers and does not pass the message on to the iframe", () => { @@ -217,7 +217,7 @@ describe("AutofillInlineMenuIframeService", () => { expect(autofillInlineMenuIframeService["portKey"]).toBe(portKey); expect( autofillInlineMenuIframeService["iframe"].contentWindow.postMessage, - ).toHaveBeenCalledWith(message, "*"); + ).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]); }); }); @@ -242,7 +242,7 @@ describe("AutofillInlineMenuIframeService", () => { expect(updateElementStylesSpy).not.toHaveBeenCalled(); expect( autofillInlineMenuIframeService["iframe"].contentWindow.postMessage, - ).toHaveBeenCalledWith(message, "*"); + ).toHaveBeenCalledWith(message, autofillInlineMenuIframeService["extensionOrigin"]); }); it("sets a light theme based on the user's system preferences", () => { @@ -262,7 +262,7 @@ describe("AutofillInlineMenuIframeService", () => { command: "initAutofillInlineMenuList", theme: ThemeType.Light, }, - "*", + autofillInlineMenuIframeService["extensionOrigin"], ); }); @@ -283,7 +283,7 @@ describe("AutofillInlineMenuIframeService", () => { command: "initAutofillInlineMenuList", theme: ThemeType.Dark, }, - "*", + autofillInlineMenuIframeService["extensionOrigin"], ); }); @@ -387,7 +387,7 @@ describe("AutofillInlineMenuIframeService", () => { command: "updateAutofillInlineMenuColorScheme", colorScheme: "normal", }, - "*", + autofillInlineMenuIframeService["extensionOrigin"], ); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts index 9a9821f643c..8b1423b1290 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/autofill-inline-menu-iframe.service.ts @@ -3,6 +3,7 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { BrowserApi } from "../../../../platform/browser/browser-api"; import { sendExtensionMessage, setElementStyles } from "../../../utils"; import { BackgroundPortMessageHandlers, @@ -15,6 +16,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe private readonly sendExtensionMessage = sendExtensionMessage; private port: chrome.runtime.Port | null = null; private portKey: string; + private readonly extensionOrigin: string; private iframeMutationObserver: MutationObserver; private iframe: HTMLIFrameElement; private ariaAlertElement: HTMLDivElement; @@ -69,6 +71,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe private iframeTitle: string, private ariaAlert?: string, ) { + this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1); this.iframeMutationObserver = new MutationObserver(this.handleMutations); } @@ -81,7 +84,7 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe * that is declared. */ initMenuIframe() { - this.defaultIframeAttributes.src = chrome.runtime.getURL("overlay/menu.html"); + this.defaultIframeAttributes.src = BrowserApi.getRuntimeURL("overlay/menu.html"); this.defaultIframeAttributes.title = this.iframeTitle; this.iframe = globalThis.document.createElement("iframe"); @@ -259,7 +262,10 @@ export class AutofillInlineMenuIframeService implements AutofillInlineMenuIframe } private postMessageToIFrame(message: any) { - this.iframe.contentWindow?.postMessage({ portKey: this.portKey, ...message }, "*"); + this.iframe.contentWindow?.postMessage( + { portKey: this.portKey, ...message }, + this.extensionOrigin, + ); } /** diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.spec.ts index 7fa07850f00..10f6c905342 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/autofill-inline-menu-button.spec.ts @@ -1,5 +1,6 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { BrowserApi } from "../../../../../platform/browser/browser-api"; import { createInitAutofillInlineMenuButtonMessageMock } from "../../../../spec/autofill-mocks"; import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils"; @@ -10,11 +11,11 @@ describe("AutofillInlineMenuButton", () => { let autofillInlineMenuButton: AutofillInlineMenuButton; const portKey: string = "inlineMenuButtonPortKey"; + const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id"; beforeEach(() => { document.body.innerHTML = ``; autofillInlineMenuButton = document.querySelector("autofill-inline-menu-button"); - autofillInlineMenuButton["messageOrigin"] = "https://localhost/"; jest.spyOn(globalThis.document, "createElement"); jest.spyOn(globalThis.parent, "postMessage"); }); @@ -56,8 +57,8 @@ describe("AutofillInlineMenuButton", () => { autofillInlineMenuButton["buttonElement"].click(); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "autofillInlineMenuButtonClicked", portKey }, - "*", + { command: "autofillInlineMenuButtonClicked", portKey, token: "test-token" }, + expectedOrigin, ); }); }); @@ -70,7 +71,7 @@ describe("AutofillInlineMenuButton", () => { it("does not post a message to close the autofill inline menu if the element is focused during the focus check", async () => { jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true); - postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" }); await flushPromises(); expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({ @@ -84,7 +85,7 @@ describe("AutofillInlineMenuButton", () => { .spyOn(autofillInlineMenuButton["buttonElement"], "querySelector") .mockReturnValue(autofillInlineMenuButton["buttonElement"]); - postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" }); await flushPromises(); expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith({ @@ -98,7 +99,7 @@ describe("AutofillInlineMenuButton", () => { jest .spyOn(autofillInlineMenuButton["buttonElement"], "querySelector") .mockReturnValue(autofillInlineMenuButton["buttonElement"]); - postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" }); await flushPromises(); globalThis.document.dispatchEvent(new MouseEvent("mouseout")); @@ -113,12 +114,12 @@ describe("AutofillInlineMenuButton", () => { jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false); jest.spyOn(autofillInlineMenuButton["buttonElement"], "querySelector").mockReturnValue(null); - postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuButtonFocused", token: "test-token" }); await flushPromises(); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "triggerDelayedAutofillInlineMenuClosure", portKey }, - "*", + { command: "triggerDelayedAutofillInlineMenuClosure", portKey, token: "test-token" }, + expectedOrigin, ); }); @@ -128,6 +129,7 @@ describe("AutofillInlineMenuButton", () => { postWindowMessage({ command: "updateAutofillInlineMenuButtonAuthStatus", authStatus: AuthenticationStatus.Unlocked, + token: "test-token", }); await flushPromises(); @@ -143,6 +145,7 @@ describe("AutofillInlineMenuButton", () => { postWindowMessage({ command: "updateAutofillInlineMenuColorScheme", colorScheme: "dark", + token: "test-token", }); await flushPromises(); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts index b4e480797da..81bf7240230 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/autofill-inline-menu-list.spec.ts @@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { BrowserApi } from "../../../../../platform/browser/browser-api"; import { InlineMenuCipherData } from "../../../../background/abstractions/overlay.background"; import { createAutofillOverlayCipherDataMock, @@ -23,6 +24,7 @@ describe("AutofillInlineMenuList", () => { let autofillInlineMenuList: AutofillInlineMenuList | null; const portKey: string = "inlineMenuListPortKey"; + const expectedOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1) || "chrome-extension://id"; const events: { eventName: any; callback: any }[] = []; beforeEach(() => { @@ -67,8 +69,8 @@ describe("AutofillInlineMenuList", () => { unlockButton.dispatchEvent(new Event("click")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "unlockVault", portKey }, - "*", + { command: "unlockVault", portKey, token: "test-token" }, + expectedOrigin, ); }); }); @@ -134,8 +136,13 @@ describe("AutofillInlineMenuList", () => { addVaultItemButton.dispatchEvent(new Event("click")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login }, - "*", + { + command: "addNewVaultItem", + portKey, + addNewCipherType: CipherType.Login, + token: "test-token", + }, + expectedOrigin, ); }); }); @@ -324,8 +331,9 @@ describe("AutofillInlineMenuList", () => { inlineMenuCipherId: "1", usePasskey: false, portKey, + token: "test-token", }, - "*", + expectedOrigin, ); }); @@ -492,8 +500,13 @@ describe("AutofillInlineMenuList", () => { viewCipherButton.dispatchEvent(new Event("click")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "viewSelectedCipher", inlineMenuCipherId: "1", portKey }, - "*", + { + command: "viewSelectedCipher", + inlineMenuCipherId: "1", + portKey, + token: "test-token", + }, + expectedOrigin, ); }); @@ -581,8 +594,13 @@ describe("AutofillInlineMenuList", () => { newVaultItemButtonSpy.dispatchEvent(new Event("click")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "addNewVaultItem", portKey, addNewCipherType: CipherType.Login }, - "*", + { + command: "addNewVaultItem", + portKey, + addNewCipherType: CipherType.Login, + token: "test-token", + }, + expectedOrigin, ); }); @@ -826,8 +844,8 @@ describe("AutofillInlineMenuList", () => { fillGeneratedPasswordButton.dispatchEvent(new Event("click")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "fillGeneratedPassword", portKey }, - "*", + { command: "fillGeneratedPassword", portKey, token: "test-token" }, + expectedOrigin, ); }); @@ -843,7 +861,7 @@ describe("AutofillInlineMenuList", () => { expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith( { command: "fillGeneratedPassword", portKey }, - "*", + expectedOrigin, ); }); @@ -857,8 +875,8 @@ describe("AutofillInlineMenuList", () => { ); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "fillGeneratedPassword", portKey }, - "*", + { command: "fillGeneratedPassword", portKey, token: "test-token" }, + expectedOrigin, ); }); @@ -896,8 +914,8 @@ describe("AutofillInlineMenuList", () => { refreshGeneratedPasswordButton.dispatchEvent(new Event("click")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "refreshGeneratedPassword", portKey }, - "*", + { command: "refreshGeneratedPassword", portKey, token: "test-token" }, + expectedOrigin, ); }); @@ -913,7 +931,7 @@ describe("AutofillInlineMenuList", () => { expect(globalThis.parent.postMessage).not.toHaveBeenCalledWith( { command: "refreshGeneratedPassword", portKey }, - "*", + expectedOrigin, ); }); @@ -927,8 +945,8 @@ describe("AutofillInlineMenuList", () => { ); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "refreshGeneratedPassword", portKey }, - "*", + { command: "refreshGeneratedPassword", portKey, token: "test-token" }, + expectedOrigin, ); }); @@ -972,7 +990,7 @@ describe("AutofillInlineMenuList", () => { it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is currently focused", () => { jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true); - postWindowMessage({ command: "checkAutofillInlineMenuListFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" }); expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); }); @@ -983,7 +1001,7 @@ describe("AutofillInlineMenuList", () => { .spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector") .mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]); - postWindowMessage({ command: "checkAutofillInlineMenuListFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" }); expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); }); @@ -994,7 +1012,7 @@ describe("AutofillInlineMenuList", () => { jest .spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector") .mockReturnValue(autofillInlineMenuList["inlineMenuListContainer"]); - postWindowMessage({ command: "checkAutofillInlineMenuListFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" }); await flushPromises(); globalThis.document.dispatchEvent(new MouseEvent("mouseout")); @@ -1010,11 +1028,11 @@ describe("AutofillInlineMenuList", () => { .spyOn(autofillInlineMenuList["inlineMenuListContainer"], "querySelector") .mockReturnValue(null); - postWindowMessage({ command: "checkAutofillInlineMenuListFocused" }); + postWindowMessage({ command: "checkAutofillInlineMenuListFocused", token: "test-token" }); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "checkAutofillInlineMenuButtonFocused", portKey }, - "*", + { command: "checkAutofillInlineMenuButtonFocused", portKey, token: "test-token" }, + expectedOrigin, ); }); @@ -1022,7 +1040,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage(createInitAutofillInlineMenuListMessageMock()); const updateCiphersSpy = jest.spyOn(autofillInlineMenuList as any, "updateListItems"); - postWindowMessage({ command: "updateAutofillInlineMenuListCiphers" }); + postWindowMessage({ command: "updateAutofillInlineMenuListCiphers", token: "test-token" }); expect(updateCiphersSpy).toHaveBeenCalled(); }); @@ -1062,7 +1080,10 @@ describe("AutofillInlineMenuList", () => { postWindowMessage(createInitAutofillInlineMenuListMessageMock()); await flushPromises(); - postWindowMessage({ command: "updateAutofillInlineMenuGeneratedPassword" }); + postWindowMessage({ + command: "updateAutofillInlineMenuGeneratedPassword", + token: "test-token", + }); expect(buildColorizedPasswordElementSpy).not.toHaveBeenCalled(); }); @@ -1074,6 +1095,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage({ command: "updateAutofillInlineMenuGeneratedPassword", generatedPassword, + token: "test-token", }); expect(buildPasswordGeneratorSpy).toHaveBeenCalled(); @@ -1090,6 +1112,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage({ command: "updateAutofillInlineMenuGeneratedPassword", generatedPassword, + token: "test-token", }); expect(buildPasswordGeneratorSpy).toHaveBeenCalledTimes(1); @@ -1115,7 +1138,7 @@ describe("AutofillInlineMenuList", () => { ); await flushPromises(); - postWindowMessage({ command: "showSaveLoginInlineMenuList" }); + postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" }); expect(buildSaveLoginInlineMenuSpy).not.toHaveBeenCalled(); }); @@ -1124,7 +1147,7 @@ describe("AutofillInlineMenuList", () => { postWindowMessage(createInitAutofillInlineMenuListMessageMock()); await flushPromises(); - postWindowMessage({ command: "showSaveLoginInlineMenuList" }); + postWindowMessage({ command: "showSaveLoginInlineMenuList", token: "test-token" }); expect(buildSaveLoginInlineMenuSpy).toHaveBeenCalled(); }); @@ -1143,7 +1166,7 @@ describe("AutofillInlineMenuList", () => { "setAttribute", ); - postWindowMessage({ command: "focusAutofillInlineMenuList" }); + postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" }); expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog"); expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true"); @@ -1161,7 +1184,7 @@ describe("AutofillInlineMenuList", () => { autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button"); jest.spyOn(unlockButton as HTMLElement, "focus"); - postWindowMessage({ command: "focusAutofillInlineMenuList" }); + postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" }); expect((unlockButton as HTMLElement).focus).toBeCalled(); }); @@ -1173,7 +1196,7 @@ describe("AutofillInlineMenuList", () => { autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button"); jest.spyOn(newItemButton as HTMLElement, "focus"); - postWindowMessage({ command: "focusAutofillInlineMenuList" }); + postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" }); expect((newItemButton as HTMLElement).focus).toBeCalled(); }); @@ -1184,7 +1207,7 @@ describe("AutofillInlineMenuList", () => { autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button"); jest.spyOn(firstCipherItem as HTMLElement, "focus"); - postWindowMessage({ command: "focusAutofillInlineMenuList" }); + postWindowMessage({ command: "focusAutofillInlineMenuList", token: "test-token" }); expect((firstCipherItem as HTMLElement).focus).toBeCalled(); }); @@ -1197,8 +1220,8 @@ describe("AutofillInlineMenuList", () => { globalThis.dispatchEvent(new Event("blur")); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "autofillInlineMenuBlurred", portKey }, - "*", + { command: "autofillInlineMenuBlurred", portKey, token: "test-token" }, + expectedOrigin, ); }); }); @@ -1220,8 +1243,13 @@ describe("AutofillInlineMenuList", () => { ); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "redirectAutofillInlineMenuFocusOut", direction: "previous", portKey }, - "*", + { + command: "redirectAutofillInlineMenuFocusOut", + direction: "previous", + portKey, + token: "test-token", + }, + expectedOrigin, ); }); @@ -1229,8 +1257,13 @@ describe("AutofillInlineMenuList", () => { globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" })); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "redirectAutofillInlineMenuFocusOut", direction: "next", portKey }, - "*", + { + command: "redirectAutofillInlineMenuFocusOut", + direction: "next", + portKey, + token: "test-token", + }, + expectedOrigin, ); }); @@ -1238,8 +1271,13 @@ describe("AutofillInlineMenuList", () => { globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" })); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "redirectAutofillInlineMenuFocusOut", direction: "current", portKey }, - "*", + { + command: "redirectAutofillInlineMenuFocusOut", + direction: "current", + portKey, + token: "test-token", + }, + expectedOrigin, ); }); }); @@ -1274,8 +1312,13 @@ describe("AutofillInlineMenuList", () => { autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]); expect(globalThis.parent.postMessage).toHaveBeenCalledWith( - { command: "updateAutofillInlineMenuListHeight", styles: { height: "300px" }, portKey }, - "*", + { + command: "updateAutofillInlineMenuListHeight", + styles: { height: "300px" }, + portKey, + token: "test-token", + }, + expectedOrigin, ); }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts index aea6ef30b99..6c61cfae6b4 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/autofill-inline-menu-container.ts @@ -1,5 +1,6 @@ import { EVENTS } from "@bitwarden/common/autofill/constants"; +import { BrowserApi } from "../../../../../platform/browser/browser-api"; import { generateRandomChars, setElementStyles } from "../../../../utils"; import { InitAutofillInlineMenuElementMessage, @@ -73,7 +74,7 @@ export class AutofillInlineMenuContainer { constructor() { this.token = generateRandomChars(32); - this.extensionOrigin = chrome.runtime.getURL("").slice(0, -1); + this.extensionOrigin = BrowserApi.getRuntimeURL("")?.slice(0, -1); globalThis.addEventListener("message", this.handleWindowMessage); } @@ -203,6 +204,9 @@ export class AutofillInlineMenuContainer { */ private handleWindowMessage = (event: MessageEvent) => { const message = event.data; + if (!message?.command) { + return; + } if (this.isForeignWindowMessage(event)) { return; } @@ -287,7 +291,10 @@ export class AutofillInlineMenuContainer { * every time the inline menu container is recreated. * */ - private isValidSessionToken(message: { token?: string }): boolean { + private isValidSessionToken(message: { token: string }): boolean { + if (!this.token || !message?.token || !message?.token.length) { + return false; + } return message.token === this.token; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts index ea77e3e434d..5df6e7cd190 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/shared/autofill-inline-menu-page-element.ts @@ -38,12 +38,8 @@ export class AutofillInlineMenuPageElement extends HTMLElement { styleSheetUrl: string, translations: Record, portKey: string, - token?: string, ): Promise { this.portKey = portKey; - if (token) { - this.token = token; - } this.translations = translations; globalThis.document.documentElement.setAttribute("lang", this.getTranslation("locale")); @@ -63,11 +59,16 @@ export class AutofillInlineMenuPageElement extends HTMLElement { * @param message - The message to post */ protected postMessageToParent(message: AutofillInlineMenuPageElementWindowMessage) { - const messageWithAuth: Record = { portKey: this.portKey, ...message }; - if (this.token) { - messageWithAuth.token = this.token; + // never send messages containing authentication tokens without a valid token and an established messageOrigin + if (!this.token || !this.messageOrigin) { + return; } - globalThis.parent.postMessage(messageWithAuth, "*"); + const messageWithAuth: Record = { + portKey: this.portKey, + ...message, + token: this.token, + }; + globalThis.parent.postMessage(messageWithAuth, this.messageOrigin); } /** @@ -105,6 +106,10 @@ export class AutofillInlineMenuPageElement extends HTMLElement { return; } + if (event.source !== globalThis.parent) { + return; + } + if (!this.messageOrigin) { this.messageOrigin = event.origin; } @@ -115,12 +120,23 @@ export class AutofillInlineMenuPageElement extends HTMLElement { const message = event?.data; - if ( - message?.token && - (message?.command === "initAutofillInlineMenuButton" || - message?.command === "initAutofillInlineMenuList") - ) { + if (!message?.command) { + return; + } + + const isInitCommand = + message.command === "initAutofillInlineMenuButton" || + message.command === "initAutofillInlineMenuList"; + + if (isInitCommand) { + if (!message?.token) { + return; + } this.token = message.token; + } else { + if (!this.token || !message?.token || message.token !== this.token) { + return; + } } const handler = this.windowMessageHandlers[message?.command]; diff --git a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap index 39ca68d912c..cfcedc9da7a 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap @@ -7,7 +7,7 @@ exports[`OverlayNotificationsContentService opening the notification bar creates >