From ab7016fd6b7fd706ba81c041d773806b3f9d581f Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:02:54 -0500 Subject: [PATCH 01/14] feat(auth): implement view data persistence in 2FA flows Add persistence to two-factor authentication in the extension login flow. Implements caching of form state to improve user experience when navigating between authentication steps. Includes feature flag for controlled rollout. --- ...actor-auth-email-component.service.spec.ts | 17 ++ ...two-factor-auth-email-component.service.ts | 12 ++ .../src/popup/services/services.module.ts | 2 +- ...o-factor-auth-authenticator.component.html | 9 +- ...two-factor-auth-authenticator.component.ts | 8 +- ...auth-component-email-cache.service.spec.ts | 165 +++++++++++++++ ...auth-email-component-cache.service.spec.ts | 165 +++++++++++++++ ...ctor-auth-email-component-cache.service.ts | 95 +++++++++ .../two-factor-auth-email.component.html | 9 +- .../two-factor-auth-email.component.ts | 40 +++- ...actor-auth-component-cache.service.spec.ts | 191 ++++++++++++++++++ ...two-factor-auth-component-cache.service.ts | 105 ++++++++++ .../two-factor-auth.component.html | 4 +- .../two-factor-auth.component.spec.ts | 18 +- .../two-factor-auth.component.ts | 92 ++++++++- libs/common/src/enums/feature-flag.enum.ts | 2 + 16 files changed, 911 insertions(+), 23 deletions(-) create mode 100644 libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts create mode 100644 libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts create mode 100644 libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts create mode 100644 libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts create mode 100644 libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts index 223375fd903..01a0129d0e5 100644 --- a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts +++ b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.spec.ts @@ -1,5 +1,6 @@ import { MockProxy, mock } from "jest-mock-extended"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; // Must mock modules before importing @@ -26,22 +27,26 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => { let dialogService: MockProxy; let window: MockProxy; + let configService: MockProxy; beforeEach(() => { jest.clearAllMocks(); dialogService = mock(); window = mock(); + configService = mock(); extensionTwoFactorAuthEmailComponentService = new ExtensionTwoFactorAuthEmailComponentService( dialogService, window, + configService, ); }); describe("openPopoutIfApprovedForEmail2fa", () => { it("should open a popout if the user confirms the warning to popout the extension when in the popup", async () => { // Arrange + configService.getFeatureFlag.mockResolvedValue(false); dialogService.openSimpleDialog.mockResolvedValue(true); jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true); @@ -61,6 +66,7 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => { it("should not open a popout if the user cancels the warning to popout the extension when in the popup", async () => { // Arrange + configService.getFeatureFlag.mockResolvedValue(false); dialogService.openSimpleDialog.mockResolvedValue(false); jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true); @@ -80,6 +86,7 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => { it("should not open a popout if not in the popup", async () => { // Arrange + configService.getFeatureFlag.mockResolvedValue(false); jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(false); // Act @@ -89,5 +96,15 @@ describe("ExtensionTwoFactorAuthEmailComponentService", () => { expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); expect(openTwoFactorAuthEmailPopout).not.toHaveBeenCalled(); }); + + it("does not prompt or open a popout if the feature flag is enabled", async () => { + configService.getFeatureFlag.mockResolvedValue(true); + jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true); + + await extensionTwoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa(); + + expect(dialogService.openSimpleDialog).not.toHaveBeenCalled(); + expect(openTwoFactorAuthEmailPopout).not.toHaveBeenCalled(); + }); }); }); diff --git a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts index 5d8d269412e..293d88c4e64 100644 --- a/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts +++ b/apps/browser/src/auth/services/extension-two-factor-auth-email-component.service.ts @@ -2,6 +2,8 @@ import { DefaultTwoFactorAuthEmailComponentService, TwoFactorAuthEmailComponentService, } from "@bitwarden/auth/angular"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { DialogService } from "@bitwarden/components"; import { openTwoFactorAuthEmailPopout } from "../../auth/popup/utils/auth-popout-window"; @@ -15,11 +17,21 @@ export class ExtensionTwoFactorAuthEmailComponentService constructor( private dialogService: DialogService, private window: Window, + private configService: ConfigService, ) { super(); } async openPopoutIfApprovedForEmail2fa(): Promise { + const isTwoFactorFormPersistenceEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + + if (isTwoFactorFormPersistenceEnabled) { + // If the feature flag is enabled, we don't need to prompt the user to open the popout + return; + } + if (BrowserPopupUtils.inPopup(this.window)) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "warning" }, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index dad4e887a12..00b8ae81cf9 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -557,7 +557,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: TwoFactorAuthEmailComponentService, useClass: ExtensionTwoFactorAuthEmailComponentService, - deps: [DialogService, WINDOW], + deps: [DialogService, WINDOW, ConfigService], }), safeProvider({ provide: TwoFactorAuthWebAuthnComponentService, diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html index af3a8569efa..b52af7b3820 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.html @@ -1,6 +1,13 @@ {{ "verificationCode" | i18n }} - + diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts index 333afc22d2a..d8735d3fd54 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-authenticator.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; +import { Component, Input, Output, EventEmitter } from "@angular/core"; import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -32,4 +32,10 @@ import { }) export class TwoFactorAuthAuthenticatorComponent { @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined; + @Output() tokenChange = new EventEmitter<{ token: string }>(); + + onTokenChange(event: Event) { + const tokenValue = (event.target as HTMLInputElement).value || ""; + this.tokenChange.emit({ token: tokenValue }); + } } diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts new file mode 100644 index 00000000000..f3b904a4ea6 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-component-email-cache.service.spec.ts @@ -0,0 +1,165 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { + TwoFactorAuthEmailComponentCache, + TwoFactorAuthEmailComponentCacheService, +} from "./two-factor-auth-email-component-cache.service"; + +describe("TwoFactorAuthEmailCache", () => { + describe("fromJSON", () => { + it("returns null when input is null", () => { + const result = TwoFactorAuthEmailComponentCache.fromJSON(null as any); + expect(result).toBeNull(); + }); + + it("creates a TwoFactorAuthEmailCache instance from valid JSON", () => { + const jsonData = { emailSent: true }; + const result = TwoFactorAuthEmailComponentCache.fromJSON(jsonData); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(TwoFactorAuthEmailComponentCache); + expect(result?.emailSent).toBe(true); + }); + }); +}); + +describe("TwoFactorAuthEmailComponentCacheService", () => { + let service: TwoFactorAuthEmailComponentCacheService; + let mockViewCacheService: MockProxy; + let mockConfigService: MockProxy; + let cacheData: BehaviorSubject; + let mockSignal: any; + + beforeEach(() => { + mockViewCacheService = mock(); + mockConfigService = mock(); + cacheData = new BehaviorSubject(null); + mockSignal = jest.fn(() => cacheData.getValue()); + mockSignal.set = jest.fn((value: TwoFactorAuthEmailComponentCache | null) => + cacheData.next(value), + ); + mockViewCacheService.signal.mockReturnValue(mockSignal); + + TestBed.configureTestingModule({ + providers: [ + TwoFactorAuthEmailComponentCacheService, + { provide: ViewCacheService, useValue: mockViewCacheService }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }); + + service = TestBed.inject(TwoFactorAuthEmailComponentCacheService); + }); + + it("creates the service", () => { + expect(service).toBeTruthy(); + }); + + describe("init", () => { + it("sets featureEnabled to true when flag is enabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + await service.init(); + + expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + + service.cacheData({ emailSent: true }); + expect(mockSignal.set).toHaveBeenCalled(); + }); + + it("sets featureEnabled to false when flag is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + await service.init(); + + expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + + service.cacheData({ emailSent: true }); + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("cacheData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("caches email sent state when feature is enabled", () => { + service.cacheData({ emailSent: true }); + + expect(mockSignal.set).toHaveBeenCalledWith({ + emailSent: true, + }); + }); + + it("does not cache data when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + service.cacheData({ emailSent: true }); + + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("clearCachedData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("clears cached data when feature is enabled", () => { + service.clearCachedData(); + + expect(mockSignal.set).toHaveBeenCalledWith(null); + }); + + it("does not clear cached data when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + service.clearCachedData(); + + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("getCachedData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("returns cached data when feature is enabled", () => { + const testData = new TwoFactorAuthEmailComponentCache(); + testData.emailSent = true; + cacheData.next(testData); + + const result = service.getCachedData(); + + expect(result).toEqual(testData); + expect(mockSignal).toHaveBeenCalled(); + }); + + it("returns null when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + const result = service.getCachedData(); + + expect(result).toBeNull(); + expect(mockSignal).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts new file mode 100644 index 00000000000..1613c0e4af8 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.spec.ts @@ -0,0 +1,165 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { + TwoFactorAuthEmailComponentCache, + TwoFactorAuthEmailComponentCacheService, +} from "./two-factor-auth-email-component-cache.service"; + +describe("TwoFactorAuthEmailComponentCache", () => { + describe("fromJSON", () => { + it("returns null when input is null", () => { + const result = TwoFactorAuthEmailComponentCache.fromJSON(null as any); + expect(result).toBeNull(); + }); + + it("creates a TwoFactorAuthEmailCache instance from valid JSON", () => { + const jsonData = { emailSent: true }; + const result = TwoFactorAuthEmailComponentCache.fromJSON(jsonData); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(TwoFactorAuthEmailComponentCache); + expect(result?.emailSent).toBe(true); + }); + }); +}); + +describe("TwoFactorAuthEmailComponentCacheService", () => { + let service: TwoFactorAuthEmailComponentCacheService; + let mockViewCacheService: MockProxy; + let mockConfigService: MockProxy; + let cacheData: BehaviorSubject; + let mockSignal: any; + + beforeEach(() => { + mockViewCacheService = mock(); + mockConfigService = mock(); + cacheData = new BehaviorSubject(null); + mockSignal = jest.fn(() => cacheData.getValue()); + mockSignal.set = jest.fn((value: TwoFactorAuthEmailComponentCache | null) => + cacheData.next(value), + ); + mockViewCacheService.signal.mockReturnValue(mockSignal); + + TestBed.configureTestingModule({ + providers: [ + TwoFactorAuthEmailComponentCacheService, + { provide: ViewCacheService, useValue: mockViewCacheService }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }); + + service = TestBed.inject(TwoFactorAuthEmailComponentCacheService); + }); + + it("creates the service", () => { + expect(service).toBeTruthy(); + }); + + describe("init", () => { + it("sets featureEnabled to true when flag is enabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + await service.init(); + + expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + + service.cacheData({ emailSent: true }); + expect(mockSignal.set).toHaveBeenCalled(); + }); + + it("sets featureEnabled to false when flag is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + await service.init(); + + expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + + service.cacheData({ emailSent: true }); + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("cacheData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("caches email sent state when feature is enabled", () => { + service.cacheData({ emailSent: true }); + + expect(mockSignal.set).toHaveBeenCalledWith({ + emailSent: true, + }); + }); + + it("does not cache data when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + service.cacheData({ emailSent: true }); + + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("clearCachedData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("clears cached data when feature is enabled", () => { + service.clearCachedData(); + + expect(mockSignal.set).toHaveBeenCalledWith(null); + }); + + it("does not clear cached data when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + service.clearCachedData(); + + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("getCachedData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("returns cached data when feature is enabled", () => { + const testData = new TwoFactorAuthEmailComponentCache(); + testData.emailSent = true; + cacheData.next(testData); + + const result = service.getCachedData(); + + expect(result).toEqual(testData); + expect(mockSignal).toHaveBeenCalled(); + }); + + it("returns null when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + const result = service.getCachedData(); + + expect(result).toBeNull(); + expect(mockSignal).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts new file mode 100644 index 00000000000..e32b6cd1385 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email-component-cache.service.ts @@ -0,0 +1,95 @@ +import { inject, Injectable, WritableSignal } from "@angular/core"; +import { Jsonify } from "type-fest"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +/** + * The key for the email two factor auth component cache. + */ +export const TWO_FACTOR_AUTH_EMAIL_COMPONENT_CACHE_KEY = "two-factor-auth-email-component-cache"; + +/** + * Cache model for the email two factor auth component. + */ +export class TwoFactorAuthEmailComponentCache { + emailSent: boolean = false; + + static fromJSON( + obj: Partial>, + ): TwoFactorAuthEmailComponentCache | null { + // Return null if the cache is empty + if (obj == null) { + return null; + } + + return Object.assign(new TwoFactorAuthEmailComponentCache(), obj); + } +} + +/** + * Cache service for the two factor auth email component. + */ +@Injectable() +export class TwoFactorAuthEmailComponentCacheService { + private viewCacheService: ViewCacheService = inject(ViewCacheService); + private configService: ConfigService = inject(ConfigService); + + /** True when the feature flag is enabled */ + private featureEnabled: boolean = false; + + /** + * Signal for the cached email state. + */ + private emailCache: WritableSignal = + this.viewCacheService.signal({ + key: TWO_FACTOR_AUTH_EMAIL_COMPONENT_CACHE_KEY, + initialValue: null, + deserializer: TwoFactorAuthEmailComponentCache.fromJSON, + }); + + /** + * Must be called once before interacting with the cached data. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + } + + /** + * Cache the email sent state. + */ + cacheData(data: { emailSent: boolean }): void { + if (!this.featureEnabled) { + return; + } + + this.emailCache.set({ + emailSent: data.emailSent, + } as TwoFactorAuthEmailComponentCache); + } + + /** + * Clear the cached email data. + */ + clearCachedData(): void { + if (!this.featureEnabled) { + return; + } + + this.emailCache.set(null); + } + + /** + * Get whether the email has been sent. + */ + getCachedData(): TwoFactorAuthEmailComponentCache | null { + if (!this.featureEnabled) { + return null; + } + + return this.emailCache(); + } +} diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html index 41873c32ed0..90f1d74ae48 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.html @@ -1,6 +1,13 @@ {{ "verificationCode" | i18n }} - +
diff --git a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts index 0c67416532f..25235017bd1 100644 --- a/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts +++ b/libs/auth/src/angular/two-factor-auth/child-components/two-factor-auth-email/two-factor-auth-email.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, Input, OnInit } from "@angular/core"; +import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core"; import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -22,6 +22,7 @@ import { ToastService, } from "@bitwarden/components"; +import { TwoFactorAuthEmailComponentCacheService } from "./two-factor-auth-email-component-cache.service"; import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-component.service"; @Component({ @@ -40,14 +41,20 @@ import { TwoFactorAuthEmailComponentService } from "./two-factor-auth-email-comp AsyncActionsModule, FormsModule, ], - providers: [], + providers: [ + { + provide: TwoFactorAuthEmailComponentCacheService, + useClass: TwoFactorAuthEmailComponentCacheService, + }, + ], }) export class TwoFactorAuthEmailComponent implements OnInit { @Input({ required: true }) tokenFormControl: FormControl | undefined = undefined; + @Output() tokenChange = new EventEmitter<{ token: string }>(); twoFactorEmail: string | undefined = undefined; - emailPromise: Promise | undefined = undefined; - tokenValue: string = ""; + emailPromise: Promise | undefined; + emailSent = false; constructor( protected i18nService: I18nService, @@ -59,14 +66,22 @@ export class TwoFactorAuthEmailComponent implements OnInit { protected appIdService: AppIdService, private toastService: ToastService, private twoFactorAuthEmailComponentService: TwoFactorAuthEmailComponentService, + private cacheService: TwoFactorAuthEmailComponentCacheService, ) {} async ngOnInit(): Promise { await this.twoFactorAuthEmailComponentService.openPopoutIfApprovedForEmail2fa?.(); + await this.cacheService.init(); + + // Check if email was already sent + const cachedData = this.cacheService.getCachedData(); + if (cachedData?.emailSent) { + this.emailSent = true; + } const providers = await this.twoFactorService.getProviders(); - if (!providers) { + if (!providers || providers.size === 0) { throw new Error("User has no 2FA Providers"); } @@ -78,11 +93,20 @@ export class TwoFactorAuthEmailComponent implements OnInit { this.twoFactorEmail = email2faProviderData.Email; - if (providers.size > 1) { + if (!this.emailSent) { await this.sendEmail(false); } } + /** + * Emits the token value to the parent component + * @param event - The event object from the input field + */ + onTokenChange(event: Event) { + const tokenValue = (event.target as HTMLInputElement).value || ""; + this.tokenChange.emit({ token: tokenValue }); + } + async sendEmail(doToast: boolean) { if (this.emailPromise !== undefined) { return; @@ -113,6 +137,10 @@ export class TwoFactorAuthEmailComponent implements OnInit { request.authRequestId = (await this.loginStrategyService.getAuthRequestId()) ?? ""; this.emailPromise = this.apiService.postTwoFactorEmail(request); await this.emailPromise; + + this.emailSent = true; + this.cacheService.cacheData({ emailSent: this.emailSent }); + if (doToast) { this.toastService.showToast({ variant: "success", diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts new file mode 100644 index 00000000000..0993954fde1 --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.spec.ts @@ -0,0 +1,191 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { + TwoFactorAuthComponentCache, + TwoFactorAuthComponentCacheService, + TwoFactorAuthComponentData, +} from "./two-factor-auth-component-cache.service"; + +describe("TwoFactorAuthCache", () => { + describe("fromJSON", () => { + it("returns null when input is null", () => { + const result = TwoFactorAuthComponentCache.fromJSON(null as any); + expect(result).toBeNull(); + }); + + it("creates a TwoFactorAuthCache instance from valid JSON", () => { + const jsonData = { + token: "123456", + remember: true, + selectedProviderType: TwoFactorProviderType.Email, + }; + const result = TwoFactorAuthComponentCache.fromJSON(jsonData as any); + + expect(result).not.toBeNull(); + expect(result).toBeInstanceOf(TwoFactorAuthComponentCache); + expect(result?.token).toBe("123456"); + expect(result?.remember).toBe(true); + expect(result?.selectedProviderType).toBe(TwoFactorProviderType.Email); + }); + }); +}); + +describe("TwoFactorAuthComponentCacheService", () => { + let service: TwoFactorAuthComponentCacheService; + let mockViewCacheService: MockProxy; + let mockConfigService: MockProxy; + let cacheData: BehaviorSubject; + let mockSignal: any; + + beforeEach(() => { + mockViewCacheService = mock(); + mockConfigService = mock(); + cacheData = new BehaviorSubject(null); + mockSignal = jest.fn(() => cacheData.getValue()); + mockSignal.set = jest.fn((value: TwoFactorAuthComponentCache | null) => cacheData.next(value)); + mockViewCacheService.signal.mockReturnValue(mockSignal); + + TestBed.configureTestingModule({ + providers: [ + TwoFactorAuthComponentCacheService, + { provide: ViewCacheService, useValue: mockViewCacheService }, + { provide: ConfigService, useValue: mockConfigService }, + ], + }); + + service = TestBed.inject(TwoFactorAuthComponentCacheService); + }); + + it("creates the service", () => { + expect(service).toBeTruthy(); + }); + + describe("init", () => { + it("sets featureEnabled to true when flag is enabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + await service.init(); + + expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + + service.cacheData({ token: "123456" }); + expect(mockSignal.set).toHaveBeenCalled(); + }); + + it("sets featureEnabled to false when flag is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + + await service.init(); + + expect(mockConfigService.getFeatureFlag).toHaveBeenCalledWith( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + + service.cacheData({ token: "123456" }); + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("cacheData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("caches complete data when feature is enabled", () => { + const testData: TwoFactorAuthComponentData = { + token: "123456", + remember: true, + selectedProviderType: TwoFactorProviderType.Email, + }; + + service.cacheData(testData); + + expect(mockSignal.set).toHaveBeenCalledWith({ + token: "123456", + remember: true, + selectedProviderType: TwoFactorProviderType.Email, + }); + }); + + it("caches partial data when feature is enabled", () => { + service.cacheData({ token: "123456" }); + + expect(mockSignal.set).toHaveBeenCalledWith({ + token: "123456", + remember: undefined, + selectedProviderType: undefined, + }); + }); + + it("does not cache data when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + service.cacheData({ token: "123456" }); + + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("clearCachedData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("clears cached data when feature is enabled", () => { + service.clearCachedData(); + + expect(mockSignal.set).toHaveBeenCalledWith(null); + }); + + it("does not clear cached data when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + service.clearCachedData(); + + expect(mockSignal.set).not.toHaveBeenCalled(); + }); + }); + + describe("getCachedData", () => { + beforeEach(async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + await service.init(); + }); + + it("returns cached data when feature is enabled", () => { + const testData = new TwoFactorAuthComponentCache(); + testData.token = "123456"; + testData.remember = true; + testData.selectedProviderType = TwoFactorProviderType.Email; + cacheData.next(testData); + + const result = service.getCachedData(); + + expect(result).toEqual(testData); + expect(mockSignal).toHaveBeenCalled(); + }); + + it("returns null when feature is disabled", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + await service.init(); + + const result = service.getCachedData(); + + expect(result).toBeNull(); + expect(mockSignal).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts new file mode 100644 index 00000000000..61b44aa98dd --- /dev/null +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth-component-cache.service.ts @@ -0,0 +1,105 @@ +import { inject, Injectable, WritableSignal } from "@angular/core"; +import { Jsonify } from "type-fest"; + +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +const TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY = "two-factor-auth-component-cache"; + +/** + * Cache model for the two factor authentication data. + */ +export class TwoFactorAuthComponentCache { + token: string | undefined = undefined; + remember: boolean | undefined = undefined; + selectedProviderType: TwoFactorProviderType | undefined = undefined; + + static fromJSON( + obj: Partial>, + ): TwoFactorAuthComponentCache | null { + // Return null if the cache is empty + if (obj == null) { + return null; + } + + return Object.assign(new TwoFactorAuthComponentCache(), obj); + } +} + +export interface TwoFactorAuthComponentData { + token?: string; + remember?: boolean; + selectedProviderType?: TwoFactorProviderType; +} + +/** + * Cache service used for the two factor auth component. + */ +@Injectable() +export class TwoFactorAuthComponentCacheService { + private viewCacheService: ViewCacheService = inject(ViewCacheService); + private configService: ConfigService = inject(ConfigService); + + /** True when the `PM9115_TwoFactorExtensionDataPersistence` flag is enabled */ + private featureEnabled: boolean = false; + + /** + * Signal for the cached TwoFactorAuthData. + */ + private twoFactorAuthComponentCache: WritableSignal = + this.viewCacheService.signal({ + key: TWO_FACTOR_AUTH_COMPONENT_CACHE_KEY, + initialValue: null, + deserializer: TwoFactorAuthComponentCache.fromJSON, + }); + + constructor() {} + + /** + * Must be called once before interacting with the cached data. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag( + FeatureFlag.PM9115_TwoFactorExtensionDataPersistence, + ); + } + + /** + * Update the cache with the new TwoFactorAuthData. + */ + cacheData(data: TwoFactorAuthComponentData): void { + if (!this.featureEnabled) { + return; + } + + this.twoFactorAuthComponentCache.set({ + token: data.token, + remember: data.remember, + selectedProviderType: data.selectedProviderType, + } as TwoFactorAuthComponentCache); + } + + /** + * Clears the cached TwoFactorAuthData. + */ + clearCachedData(): void { + if (!this.featureEnabled) { + return; + } + + this.twoFactorAuthComponentCache.set(null); + } + + /** + * Returns the cached TwoFactorAuthData (when available). + */ + getCachedData(): TwoFactorAuthComponentCache | null { + if (!this.featureEnabled) { + return null; + } + + return this.twoFactorAuthComponentCache(); + } +} diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html index ec03944a954..8ad35c7b5c6 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.html @@ -13,11 +13,13 @@ > {{ "dontAskAgainOnThisDeviceFor30Days" | i18n }} - + { let anonLayoutWrapperDataService: MockProxy; let mockEnvService: MockProxy; let mockLoginSuccessHandlerService: MockProxy; + let mockTwoFactorAuthCompCacheService: MockProxy; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -112,6 +112,10 @@ describe("TwoFactorAuthComponent", () => { anonLayoutWrapperDataService = mock(); + mockTwoFactorAuthCompCacheService = mock(); + mockTwoFactorAuthCompCacheService.getCachedData.mockReturnValue(null); + mockTwoFactorAuthCompCacheService.init.mockResolvedValue(); + mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ hasMasterPassword: false, @@ -155,7 +159,9 @@ describe("TwoFactorAuthComponent", () => { }), }; - selectedUserDecryptionOptions = new BehaviorSubject(undefined); + selectedUserDecryptionOptions = new BehaviorSubject( + mockUserDecryptionOpts.withMasterPassword, + ); mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; TestBed.configureTestingModule({ @@ -194,6 +200,10 @@ describe("TwoFactorAuthComponent", () => { { provide: EnvironmentService, useValue: mockEnvService }, { provide: AnonLayoutWrapperDataService, useValue: anonLayoutWrapperDataService }, { provide: LoginSuccessHandlerService, useValue: mockLoginSuccessHandlerService }, + { + provide: TwoFactorAuthComponentCacheService, + useValue: mockTwoFactorAuthCompCacheService, + }, ], }); 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 aed9f9f07a5..f09d7163667 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 @@ -60,6 +60,10 @@ import { TwoFactorAuthDuoComponent } from "./child-components/two-factor-auth-du import { TwoFactorAuthEmailComponent } from "./child-components/two-factor-auth-email/two-factor-auth-email.component"; import { TwoFactorAuthWebAuthnComponent } from "./child-components/two-factor-auth-webauthn/two-factor-auth-webauthn.component"; import { TwoFactorAuthYubikeyComponent } from "./child-components/two-factor-auth-yubikey.component"; +import { + TwoFactorAuthComponentCacheService, + TwoFactorAuthComponentData, +} from "./two-factor-auth-component-cache.service"; import { DuoLaunchAction, LegacyKeyMigrationAction, @@ -90,7 +94,11 @@ import { TwoFactorAuthYubikeyComponent, TwoFactorAuthWebAuthnComponent, ], - providers: [], + providers: [ + { + provide: TwoFactorAuthComponentCacheService, + }, + ], }) export class TwoFactorAuthComponent implements OnInit, OnDestroy { @ViewChild("continueButton", { read: ElementRef, static: false }) continueButton: @@ -160,6 +168,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, private environmentService: EnvironmentService, private loginSuccessHandlerService: LoginSuccessHandlerService, + private twoFactorAuthComponentCacheService: TwoFactorAuthComponentCacheService, ) {} async ngOnInit() { @@ -168,7 +177,33 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { this.listenForAuthnSessionTimeout(); - await this.setSelected2faProviderType(); + // Initialize the cache + await this.twoFactorAuthComponentCacheService.init(); + + // Load cached form data if available + let loadedCachedProviderType = false; + const cachedData = this.twoFactorAuthComponentCacheService.getCachedData(); + if (cachedData) { + if (cachedData.token) { + this.form.patchValue({ token: cachedData.token }); + } + if (cachedData.remember !== undefined) { + this.form.patchValue({ remember: cachedData.remember }); + } + if (cachedData.selectedProviderType !== undefined) { + this.selectedProviderType = cachedData.selectedProviderType; + loadedCachedProviderType = true; + } + } + + // If we don't have a cached provider type, set it to the default and cache it + if (!loadedCachedProviderType) { + this.selectedProviderType = await this.initializeSelected2faProviderType(); + this.twoFactorAuthComponentCacheService.cacheData({ + selectedProviderType: this.selectedProviderType, + }); + } + await this.set2faProvidersAndData(); await this.setAnonLayoutDataByTwoFactorProviderType(); @@ -181,7 +216,29 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { this.loading = false; } - private async setSelected2faProviderType() { + /** + * Save specific form data fields to the cache + */ + async saveFormDataWithPartialData(data: Partial) { + // Get current cached data + const currentData = this.twoFactorAuthComponentCacheService.getCachedData(); + + this.twoFactorAuthComponentCacheService.cacheData({ + token: data?.token ?? currentData?.token ?? "", + remember: data?.remember ?? currentData?.remember ?? false, + selectedProviderType: data?.selectedProviderType ?? currentData?.selectedProviderType, + }); + } + + /** + * Save the remember value to the cache when the checkbox is checked or unchecked + */ + async onRememberChange() { + const rememberValue = !!this.rememberFormControl.value; + await this.saveFormDataWithPartialData({ remember: rememberValue }); + } + + private async initializeSelected2faProviderType(): Promise { const webAuthnSupported = this.platformUtilsService.supportsWebAuthn(this.win); if ( @@ -190,18 +247,19 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { ) { const webAuthn2faResponse = this.activatedRoute.snapshot.paramMap.get("webAuthnResponse"); if (webAuthn2faResponse) { - this.selectedProviderType = TwoFactorProviderType.WebAuthn; - return; + return TwoFactorProviderType.WebAuthn; } } - this.selectedProviderType = await this.twoFactorService.getDefaultProvider(webAuthnSupported); + return await this.twoFactorService.getDefaultProvider(webAuthnSupported); } private async set2faProvidersAndData() { this.twoFactorProviders = await this.twoFactorService.getProviders(); - const providerData = this.twoFactorProviders?.get(this.selectedProviderType); - this.selectedProviderData = providerData; + if (this.selectedProviderType !== undefined) { + const providerData = this.twoFactorProviders?.get(this.selectedProviderType); + this.selectedProviderData = providerData; + } } private listenForAuthnSessionTimeout() { @@ -267,6 +325,13 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { // In all flows but WebAuthn, the remember value is taken from the form. const rememberValue = remember ?? this.rememberFormControl.value ?? false; + // Cache form data before submitting + this.twoFactorAuthComponentCacheService.cacheData({ + token: tokenValue, + remember: rememberValue, + selectedProviderType: this.selectedProviderType, + }); + try { this.formPromise = this.loginStrategyService.logInTwoFactor( new TokenTwoFactorRequest(this.selectedProviderType, tokenValue, rememberValue), @@ -274,6 +339,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { ); const authResult: AuthResult = await this.formPromise; this.logService.info("Successfully submitted two factor token"); + await this.handleAuthResult(authResult); } catch { this.logService.error("Error submitting two factor token"); @@ -299,6 +365,13 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { this.selectedProviderType = response.type; await this.setAnonLayoutDataByTwoFactorProviderType(); + // Update the cached provider type when a new one is chosen + this.twoFactorAuthComponentCacheService.cacheData({ + token: "", + remember: false, + selectedProviderType: response.type, + }); + this.form.reset(); this.form.updateValueAndValidity(); } @@ -376,6 +449,9 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { } private async handleAuthResult(authResult: AuthResult) { + // Clear form cache + this.twoFactorAuthComponentCacheService.clearCachedData(); + if (await this.handleMigrateEncryptionKey(authResult)) { return; // stop login process } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index e353d79988f..affa7ccff4f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -17,6 +17,7 @@ export enum FeatureFlag { /* Auth */ PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence", + PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence", /* Autofill */ BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", @@ -116,6 +117,7 @@ export const DefaultFeatureFlagValue = { /* Auth */ [FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE, + [FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE, /* Billing */ [FeatureFlag.TrialPaymentOptional]: FALSE, From 4943e7096554ac5fb0d3a5b4aa38a92d5ee78707 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Fri, 25 Apr 2025 11:16:00 -0400 Subject: [PATCH 02/14] Pm 19366 handle selections in notification dropdown component to save ciphers appropriately (#14070) * initial approach for folder selection via queryselect * handle folder selection with signals * custom signal when needed on option select to track individual select values * add vault signal * initial approach for collection data * different calls for collections, add collection signal, alter approach * add appropriate icon for collections dropdown * populate vault with notification queue * org id added to extension message type * clean up naming for upcoming change * use reduce in getCollections --- .../abstractions/notification.background.ts | 11 +++- .../notification.background.spec.ts | 3 + .../background/notification.background.ts | 57 +++++++++++++++++-- .../content/components/common-types.ts | 6 ++ .../components/notification/button-row.ts | 38 ++++++++++++- .../components/notification/container.ts | 5 +- .../content/components/notification/footer.ts | 5 +- .../option-selection/option-selection.ts | 5 +- .../content/components/rows/button-row.ts | 4 +- .../components/signals/selected-collection.ts | 3 + .../components/signals/selected-folder.ts | 3 + .../components/signals/selected-vault.ts | 3 + .../abstractions/notification-bar.ts | 7 ++- apps/browser/src/autofill/notification/bar.ts | 38 +++++++++++-- .../browser/src/background/main.background.ts | 1 + 15 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/signals/selected-collection.ts create mode 100644 apps/browser/src/autofill/content/components/signals/selected-folder.ts create mode 100644 apps/browser/src/autofill/content/components/signals/selected-vault.ts diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index c93fd9a3acf..db110319d20 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -2,6 +2,7 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { CollectionView } from "../../content/components/common-types"; import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum"; import AutofillPageDetails from "../../models/autofill-page-details"; @@ -83,6 +84,7 @@ type NotificationBackgroundExtensionMessage = { tab?: chrome.tabs.Tab; sender?: string; notificationType?: string; + organizationId?: string; fadeOutNotification?: boolean; }; @@ -94,6 +96,10 @@ type NotificationBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; + bgGetCollectionData: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgOpenAtRisksPasswords: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; @@ -101,11 +107,14 @@ type NotificationBackgroundExtensionMessageHandlers = { bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgRemoveTabFromNotificationQueue: ({ sender }: BackgroundSenderParam) => void; bgSaveCipher: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + bgOpenAddEditVaultItemPopout: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; bgOpenViewVaultItemPopout: ({ message, sender, }: BackgroundOnMessageHandlerParams) => Promise; - bgOpenVault: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgNeverSave: ({ sender }: BackgroundSenderParam) => Promise; bgUnlockPopoutOpened: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgReopenUnlockPopout: ({ sender }: BackgroundSenderParam) => Promise; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index bb993fcf94b..63ae1193737 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom, of } from "rxjs"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/policy/default-policy.service"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -53,6 +54,7 @@ describe("NotificationBackground", () => { let notificationBackground: NotificationBackground; const autofillService = mock(); const cipherService = mock(); + const collectionService = mock(); let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; const policyService = mock(); @@ -83,6 +85,7 @@ describe("NotificationBackground", () => { authService, autofillService, cipherService, + collectionService, configService, domainSettingsService, environmentService, diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 9083f15d4f2..4e2a99d4a7a 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { firstValueFrom, switchMap, map, of } from "rxjs"; +import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -50,6 +51,7 @@ import { OrganizationCategories, NotificationCipherData, } from "../content/components/cipher/types"; +import { CollectionView } from "../content/components/common-types"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; @@ -92,9 +94,11 @@ export default class NotificationBackground { bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), bgGetExcludedDomains: () => this.getExcludedDomains(), bgGetFolderData: () => this.getFolderData(), + bgGetCollectionData: ({ message }) => this.getCollectionData(message), bgGetOrgData: () => this.getOrgData(), bgNeverSave: ({ sender }) => this.saveNever(sender.tab), - bgOpenVault: ({ message, sender }) => this.openVault(message, sender.tab), + bgOpenAddEditVaultItemPopout: ({ message, sender }) => + this.openAddEditVaultItem(message, sender.tab), bgOpenViewVaultItemPopout: ({ message, sender }) => this.viewItem(message, sender.tab), bgRemoveTabFromNotificationQueue: ({ sender }) => this.removeTabFromNotificationQueue(sender.tab), @@ -114,6 +118,7 @@ export default class NotificationBackground { private authService: AuthService, private autofillService: AutofillService, private cipherService: CipherService, + private collectionService: CollectionService, private configService: ConfigService, private domainSettingsService: DomainSettingsService, private environmentService: EnvironmentService, @@ -789,17 +794,36 @@ export default class NotificationBackground { userId, ); - await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id }); + await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView?.id }); } - private async openVault( + private async openAddEditVaultItem( message: NotificationBackgroundExtensionMessage, senderTab: chrome.tabs.Tab, ) { - if (!message.cipherId) { - await this.openAddEditVaultItemPopout(senderTab); + const { cipherId, organizationId } = message; + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getOptionalUserId)); + if (cipherId) { + await this.openAddEditVaultItemPopout(senderTab, { cipherId }); + return; } - await this.openAddEditVaultItemPopout(senderTab, { cipherId: message.cipherId }); + + const queueItem = this.notificationQueue.find((item) => item.tab.id === senderTab.id); + + if (queueItem?.type === NotificationQueueMessageType.AddLogin) { + const cipherView = this.convertAddLoginQueueMessageToCipherView(queueItem); + cipherView.organizationId = organizationId; + + if (userId) { + await this.cipherService.setAddEditCipherInfo({ cipher: cipherView }, userId); + } + + await this.openAddEditVaultItemPopout(senderTab); + this.removeTabFromNotificationQueue(senderTab); + return; + } + + await this.openAddEditVaultItemPopout(senderTab); } private async viewItem( @@ -898,6 +922,25 @@ export default class NotificationBackground { return await firstValueFrom(this.folderService.folderViews$(activeUserId)); } + private async getCollectionData( + message: NotificationBackgroundExtensionMessage, + ): Promise { + const collections = (await this.collectionService.getAllDecrypted()).reduce( + (acc, collection) => { + if (collection.organizationId === message?.orgId) { + acc.push({ + id: collection.id, + name: collection.name, + organizationId: collection.organizationId, + }); + } + return acc; + }, + [], + ); + return collections; + } + private async getWebVaultUrl(): Promise { const env = await firstValueFrom(this.environmentService.environment$); return env.getWebVaultUrl(); @@ -924,6 +967,7 @@ export default class NotificationBackground { const organizations = await firstValueFrom( this.organizationService.organizations$(activeUserId), ); + return organizations.map((org) => { const { id, name, productTierType } = org; return { @@ -1054,6 +1098,7 @@ export default class NotificationBackground { cipherView.folderId = folderId; cipherView.type = CipherType.Login; cipherView.login = loginView; + cipherView.organizationId = null; return cipherView; } diff --git a/apps/browser/src/autofill/content/components/common-types.ts b/apps/browser/src/autofill/content/components/common-types.ts index df11e140d70..591c579bae5 100644 --- a/apps/browser/src/autofill/content/components/common-types.ts +++ b/apps/browser/src/autofill/content/components/common-types.ts @@ -26,3 +26,9 @@ export type OrgView = { name: string; productTierType?: ProductTierType; }; + +export type CollectionView = { + id: string; + name: string; + organizationId: string; +}; diff --git a/apps/browser/src/autofill/content/components/notification/button-row.ts b/apps/browser/src/autofill/content/components/notification/button-row.ts index 3834da4269d..e181a6096f9 100644 --- a/apps/browser/src/autofill/content/components/notification/button-row.ts +++ b/apps/browser/src/autofill/content/components/notification/button-row.ts @@ -3,9 +3,12 @@ import { html } from "lit"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { Theme } from "@bitwarden/common/platform/enums"; -import { Option, OrgView, FolderView } from "../common-types"; -import { Business, Family, Folder, User } from "../icons"; +import { Option, OrgView, FolderView, CollectionView } from "../common-types"; +import { Business, Family, Folder, User, CollectionShared } from "../icons"; import { ButtonRow } from "../rows/button-row"; +import { selectedCollection as selectedCollectionSignal } from "../signals/selected-collection"; +import { selectedFolder as selectedFolderSignal } from "../signals/selected-folder"; +import { selectedVault as selectedVaultSignal } from "../signals/selected-vault"; function getVaultIconByProductTier(productTierType?: ProductTierType): Option["icon"] { switch (productTierType) { @@ -29,11 +32,13 @@ export type NotificationButtonRowProps = { text: string; handlePrimaryButtonClick: (args: any) => void; }; + collections?: CollectionView[]; theme: Theme; }; export function NotificationButtonRow({ folders, + collections, i18n, organizations, primaryButton, @@ -77,6 +82,21 @@ export function NotificationButtonRow({ ) : []; + const collectionOptions: Option[] = collections?.length + ? collections.reduce( + (options, { id, name }: any) => [ + ...options, + { + icon: CollectionShared, + text: name, + value: id === null ? "0" : id, + default: id === null, + }, + ], + [], + ) + : []; + return html` ${ButtonRow({ theme, @@ -88,15 +108,27 @@ export function NotificationButtonRow({ id: "organization", label: i18n.vault, options: organizationOptions, + selectedSignal: selectedVaultSignal, }, ] : []), - ...(folderOptions.length > 1 + ...(folderOptions.length > 1 && !collectionOptions.length ? [ { id: "folder", label: i18n.folder, options: folderOptions, + selectedSignal: selectedFolderSignal, + }, + ] + : []), + ...(collectionOptions.length > 1 + ? [ + { + id: "collection", + label: "Collection", // @TODO localize + options: collectionOptions, + selectedSignal: selectedCollectionSignal, }, ] : []), diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 5d9399eab70..44264816fe7 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -9,7 +9,7 @@ import { NotificationType, } from "../../../notification/abstractions/notification-bar"; import { NotificationCipherData } from "../cipher/types"; -import { FolderView, OrgView } from "../common-types"; +import { CollectionView, FolderView, OrgView } from "../common-types"; import { themes, spacing } from "../constants/styles"; import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body"; @@ -25,6 +25,7 @@ export type NotificationContainerProps = NotificationBarIframeInitData & { handleEditOrUpdateAction: (e: Event) => void; } & { ciphers?: NotificationCipherData[]; + collections?: CollectionView[]; folders?: FolderView[]; i18n: { [key: string]: string }; organizations?: OrgView[]; @@ -36,6 +37,7 @@ export function NotificationContainer({ handleEditOrUpdateAction, handleSaveAction, ciphers, + collections, folders, i18n, organizations, @@ -64,6 +66,7 @@ export function NotificationContainer({ : null} ${NotificationFooter({ handleSaveAction, + collections, folders, i18n, notificationType: type, diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts index 58a87ebc678..40c3dcecf41 100644 --- a/apps/browser/src/autofill/content/components/notification/footer.ts +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -7,12 +7,13 @@ import { NotificationType, NotificationTypes, } from "../../../notification/abstractions/notification-bar"; -import { OrgView, FolderView } from "../common-types"; +import { OrgView, FolderView, CollectionView } from "../common-types"; import { spacing, themes } from "../constants/styles"; import { NotificationButtonRow } from "./button-row"; export type NotificationFooterProps = { + collections?: CollectionView[]; folders?: FolderView[]; i18n: { [key: string]: string }; notificationType?: NotificationType; @@ -22,6 +23,7 @@ export type NotificationFooterProps = { }; export function NotificationFooter({ + collections, folders, i18n, notificationType, @@ -36,6 +38,7 @@ export function NotificationFooter({
${!isChangeNotification ? NotificationButtonRow({ + collections, folders, organizations, i18n, diff --git a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts index 5f43e7a0256..49b51852a39 100644 --- a/apps/browser/src/autofill/content/components/option-selection/option-selection.ts +++ b/apps/browser/src/autofill/content/components/option-selection/option-selection.ts @@ -32,6 +32,9 @@ export class OptionSelection extends LitElement { @property({ type: (selectedOption: Option["value"]) => selectedOption }) handleSelectionUpdate?: (args: any) => void; + @property({ attribute: false }) + selectedSignal?: { set: (value: any) => void }; + @state() private showMenu = false; @@ -77,7 +80,7 @@ export class OptionSelection extends LitElement { private handleOptionSelection = (selectedOption: Option) => { this.showMenu = false; this.selection = selectedOption; - + this.selectedSignal?.set(selectedOption.value); // Any side-effects that should occur from the selection this.handleSelectionUpdate?.(selectedOption.value); }; diff --git a/apps/browser/src/autofill/content/components/rows/button-row.ts b/apps/browser/src/autofill/content/components/rows/button-row.ts index 80dcd0de125..f6674da6b6e 100644 --- a/apps/browser/src/autofill/content/components/rows/button-row.ts +++ b/apps/browser/src/autofill/content/components/rows/button-row.ts @@ -19,6 +19,7 @@ export type ButtonRowProps = { label?: string; options: Option[]; handleSelectionUpdate?: (args: any) => void; + selectedSignal?: { set: (value: any) => void }; }[]; }; @@ -32,7 +33,7 @@ export function ButtonRow({ theme, primaryButton, selectButtons }: ButtonRowProp })}
${selectButtons?.map( - ({ id, label, options, handleSelectionUpdate }) => + ({ id, label, options, handleSelectionUpdate, selectedSignal }) => html` ` || nothing, )} diff --git a/apps/browser/src/autofill/content/components/signals/selected-collection.ts b/apps/browser/src/autofill/content/components/signals/selected-collection.ts new file mode 100644 index 00000000000..7e6a8d69e3a --- /dev/null +++ b/apps/browser/src/autofill/content/components/signals/selected-collection.ts @@ -0,0 +1,3 @@ +import { signal } from "@lit-labs/signals"; + +export const selectedCollection = signal("0"); diff --git a/apps/browser/src/autofill/content/components/signals/selected-folder.ts b/apps/browser/src/autofill/content/components/signals/selected-folder.ts new file mode 100644 index 00000000000..4c9e30521d5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/signals/selected-folder.ts @@ -0,0 +1,3 @@ +import { signal } from "@lit-labs/signals"; + +export const selectedFolder = signal("0"); diff --git a/apps/browser/src/autofill/content/components/signals/selected-vault.ts b/apps/browser/src/autofill/content/components/signals/selected-vault.ts new file mode 100644 index 00000000000..d74549b1c43 --- /dev/null +++ b/apps/browser/src/autofill/content/components/signals/selected-vault.ts @@ -0,0 +1,3 @@ +import { signal } from "@lit-labs/signals"; + +export const selectedVault = signal("0"); diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 9dd02b64154..8256190ea55 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -1,7 +1,11 @@ import { Theme } from "@bitwarden/common/platform/enums"; import { NotificationCipherData } from "../../../autofill/content/components/cipher/types"; -import { FolderView, OrgView } from "../../../autofill/content/components/common-types"; +import { + FolderView, + OrgView, + CollectionView, +} from "../../../autofill/content/components/common-types"; const NotificationTypes = { Add: "add", @@ -19,6 +23,7 @@ type NotificationTaskInfo = { type NotificationBarIframeInitData = { ciphers?: NotificationCipherData[]; folders?: FolderView[]; + collections?: CollectionView[]; importType?: string; isVaultLocked?: boolean; launchTimestamp?: number; diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index a70eb08be0e..14d9bcd6d0f 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -6,9 +6,11 @@ import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background"; import { NotificationCipherData } from "../content/components/cipher/types"; -import { OrgView } from "../content/components/common-types"; +import { CollectionView, OrgView } from "../content/components/common-types"; import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container"; import { NotificationContainer } from "../content/components/notification/container"; +import { selectedFolder as selectedFolderSignal } from "../content/components/signals/selected-folder"; +import { selectedVault as selectedVaultSignal } from "../content/components/signals/selected-vault"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -41,6 +43,7 @@ function load() { applyNotificationBarStyle(); }); } + function applyNotificationBarStyle() { if (!useComponentBar) { // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -140,7 +143,7 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { document.body.innerHTML = ""; // Current implementations utilize a require for scss files which creates the need to remove the node. document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove()); - + const orgId = selectedVaultSignal.get(); await Promise.all([ new Promise((resolve) => sendPlatformMessage({ command: "bgGetOrgData" }, resolve), @@ -151,13 +154,18 @@ async function initNotificationBar(message: NotificationBarWindowMessage) { new Promise((resolve) => sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, resolve), ), - ]).then(([organizations, folders, ciphers]) => { + new Promise((resolve) => + sendPlatformMessage({ command: "bgGetCollectionData", orgId }, resolve), + ), + ]).then(([organizations, folders, ciphers, collections]) => { notificationBarIframeInitData = { ...notificationBarIframeInitData, + organizations, folders, ciphers, - organizations, + collections, }; + // @TODO use context to avoid prop drilling return render( NotificationContainer({ @@ -254,9 +262,18 @@ function handleCloseNotification(e: Event) { } function handleSaveAction(e: Event) { + const selectedVault = selectedVaultSignal.get(); + if (selectedVault.length > 1) { + openAddEditVaultItemPopout(e, { organizationId: selectedVault }); + handleCloseNotification(e); + return; + } + e.preventDefault(); - sendSaveCipherMessage(removeIndividualVault()); + const selectedFolder = selectedFolderSignal.get(); + + sendSaveCipherMessage(removeIndividualVault(), selectedFolder); if (removeIndividualVault()) { return; } @@ -351,6 +368,17 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM ); } +function openAddEditVaultItemPopout( + e: Event, + options: { cipherId?: string; organizationId?: string }, +) { + e.preventDefault(); + sendPlatformMessage({ + command: "bgOpenAddEditVaultItemPopout", + ...options, + }); +} + function openViewVaultItemPopout(cipherId: string) { sendPlatformMessage({ command: "bgOpenViewVaultItemPopout", diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 3066ef5eef5..da47542ee6b 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1206,6 +1206,7 @@ export default class MainBackground { this.authService, this.autofillService, this.cipherService, + this.collectionService, this.configService, this.domainSettingsService, this.environmentService, From e4ba98f2d0ca5b90a8ac1d475f1c91f8a0744196 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 25 Apr 2025 17:48:32 +0200 Subject: [PATCH 03/14] Remove key.key.bytelength check in encryptUint8array (#14432) --- .../crypto/services/encrypt.service.implementation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index fceef34421c..0b6a6b641ae 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -117,7 +117,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64) { throw new Error("Type 0 encryption is not supported."); } } From 9d95f9af032d15649d0ec2200e533e78b0596b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Fri, 25 Apr 2025 18:16:15 +0200 Subject: [PATCH 04/14] [PM-20597] Fix linux desktop_native script (#14428) * Fix linux desktop_native build script * Add linux variables * Remove default * Remove unused import * Update apps/desktop/desktop_native/build.js Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- apps/desktop/desktop_native/build.js | 71 +++++++++++++--------------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index ec20dce4116..da61da15f9d 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -3,6 +3,21 @@ const child_process = require("child_process"); const fs = require("fs"); const path = require("path"); const process = require("process"); + +// Map of the Node arch equivalents for the rust target triplets, used to move the file to the correct location +const rustTargetsMap = { + "i686-pc-windows-msvc": { nodeArch: 'ia32', platform: 'win32' }, + "x86_64-pc-windows-msvc": { nodeArch: 'x64', platform: 'win32' }, + "aarch64-pc-windows-msvc": { nodeArch: 'arm64', platform: 'win32' }, + "x86_64-apple-darwin": { nodeArch: 'x64', platform: 'darwin' }, + "aarch64-apple-darwin": { nodeArch: 'arm64', platform: 'darwin' }, + 'x86_64-unknown-linux-musl': { nodeArch: 'x64', platform: 'linux' }, + 'aarch64-unknown-linux-musl': { nodeArch: 'arm64', platform: 'linux' }, +} + +// Ensure the dist directory exists +fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true }); + const args = process.argv.slice(2); // Get arguments passed to the script const mode = args.includes("--release") ? "release" : "debug"; const targetArg = args.find(arg => arg.startsWith("--target=")); @@ -13,13 +28,21 @@ let crossPlatform = process.argv.length > 2 && process.argv[2] === "cross-platfo function buildNapiModule(target, release = true) { const targetArg = target ? `--target ${target}` : ""; const releaseArg = release ? "--release" : ""; - return child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") }); + child_process.execSync(`npm run build -- ${releaseArg} ${targetArg}`, { stdio: 'inherit', cwd: path.join(__dirname, "napi") }); } function buildProxyBin(target, release = true) { const targetArg = target ? `--target ${target}` : ""; const releaseArg = release ? "--release" : ""; - return child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); + child_process.execSync(`cargo build --bin desktop_proxy ${releaseArg} ${targetArg}`, {stdio: 'inherit', cwd: path.join(__dirname, "proxy")}); + + if (target) { + // Copy the resulting binary to the dist folder + const targetFolder = release ? "release" : "debug"; + const ext = process.platform === "win32" ? ".exe" : ""; + const nodeArch = rustTargetsMap[target].nodeArch; + fs.copyFileSync(path.join(__dirname, "target", target, targetFolder, `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`)); + } } if (!crossPlatform && !target) { @@ -36,45 +59,17 @@ if (target) { return; } -// Note that targets contains pairs of [rust target, node arch] -// We do this to move the output binaries to a location that can -// be easily accessed from electron-builder using ${os} and ${arch} -let targets = []; -switch (process.platform) { - case "win32": - targets = [ - ["i686-pc-windows-msvc", 'ia32'], - ["x86_64-pc-windows-msvc", 'x64'], - ["aarch64-pc-windows-msvc", 'arm64'] - ]; - break; +// Filter the targets based on the current platform, and build for each of them +let platformTargets = Object.entries(rustTargetsMap).filter(([_, { platform: p }]) => p === process.platform); +console.log("Cross building native modules for the targets: ", platformTargets.map(([target, _]) => target).join(", ")); - case "darwin": - targets = [ - ["x86_64-apple-darwin", 'x64'], - ["aarch64-apple-darwin", 'arm64'] - ]; - break; - - default: - targets = [ - ['x86_64-unknown-linux-musl', 'x64'], - ['aarch64-unknown-linux-musl', 'arm64'] - ]; - - process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; - process.env["PKG_CONFIG_ALL_STATIC"] = "1"; - break; +// When building for Linux, we need to set some environment variables to allow cross-compilation +if (process.platform === "linux") { + process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; + process.env["PKG_CONFIG_ALL_STATIC"] = "1"; } -console.log("Cross building native modules for the targets: ", targets.map(([target, _]) => target).join(", ")); - -fs.mkdirSync(path.join(__dirname, "dist"), { recursive: true }); - -targets.forEach(([target, nodeArch]) => { +platformTargets.forEach(([target, _]) => { buildNapiModule(target); buildProxyBin(target); - - const ext = process.platform === "win32" ? ".exe" : ""; - fs.copyFileSync(path.join(__dirname, "target", target, "release", `desktop_proxy${ext}`), path.join(__dirname, "dist", `desktop_proxy.${process.platform}-${nodeArch}${ext}`)); }); From aafc82e1b8e59a11070d30e472d7db73697a44d9 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 25 Apr 2025 19:11:39 +0200 Subject: [PATCH 05/14] Remove direct key buffer access in encrypt service test (#14434) --- .../crypto/services/encrypt.service.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index bc945a5eff7..69a628f9604 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -58,10 +58,9 @@ describe("EncryptService", () => { it("fails if type 0 key is provided with flag turned on", async () => { (encryptService as any).blockType0 = true; const mock32Key = mock(); - mock32Key.key = makeStaticByteArray(32); mock32Key.inner.mockReturnValue({ type: 0, - encryptionKey: mock32Key.key, + encryptionKey: makeStaticByteArray(32), }); await expect(encryptService.wrapSymmetricKey(mock32Key, mock32Key)).rejects.toThrow( @@ -99,10 +98,9 @@ describe("EncryptService", () => { it("throws if type 0 key is provided with flag turned on", async () => { (encryptService as any).blockType0 = true; const mock32Key = mock(); - mock32Key.key = makeStaticByteArray(32); mock32Key.inner.mockReturnValue({ type: 0, - encryptionKey: mock32Key.key, + encryptionKey: makeStaticByteArray(32), }); await expect( @@ -140,10 +138,9 @@ describe("EncryptService", () => { it("throws if type 0 key is provided with flag turned on", async () => { (encryptService as any).blockType0 = true; const mock32Key = mock(); - mock32Key.key = makeStaticByteArray(32); mock32Key.inner.mockReturnValue({ type: 0, - encryptionKey: mock32Key.key, + encryptionKey: makeStaticByteArray(32), }); await expect( From b4c4eea2290abafa0d2949f4865e9fbaa7ba01dd Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 25 Apr 2025 13:15:15 -0400 Subject: [PATCH 06/14] Removed feature flag (#14410) --- libs/common/src/enums/feature-flag.enum.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index affa7ccff4f..4eabb8ea114 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,7 +55,6 @@ export enum FeatureFlag { PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form", NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", - VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", PM18520_UpdateDesktopCipherForm = "pm-18520-desktop-cipher-forms", @@ -109,7 +108,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE, [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, - [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.SecurityTasks]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.PM18520_UpdateDesktopCipherForm]: FALSE, From fd0db40f79b8431b2bf4057664a6fe74ef6dc392 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 25 Apr 2025 19:26:39 +0200 Subject: [PATCH 07/14] [PM-20492] Refactor symmetric keys - remove key buffer representation, migrate consumers to .toEncoded() (#14371) * Refactor encrypt service to expose key wrapping * Fix build * Undo ts strict removal * Fix wrong method being used to encrypt key material * Rename parameters and remove todo * Add summary to encrypt * Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/abstractions/encrypt.service.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Add tests for unhappy paths * Add test coverage * Add links * Remove direct buffer access * Fix build on cli --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../admin-console/commands/confirm.command.ts | 2 +- .../setup/setup-business-unit.component.ts | 6 ++---- .../auth-request/auth-request.service.spec.ts | 6 ++++-- .../common/services/pin/pin.service.spec.ts | 2 +- .../webauthn-login-prf-key.service.spec.ts | 2 +- .../services/encrypt.service.implementation.ts | 6 +++--- .../crypto/services/encrypt.service.spec.ts | 10 ++++------ .../device-trust.service.implementation.ts | 4 ++-- .../services/device-trust.service.spec.ts | 16 +++++++++------- .../models/domain/symmetric-crypto-key.spec.ts | 2 -- .../models/domain/symmetric-crypto-key.ts | 3 --- .../services/key-generation.service.spec.ts | 9 +++++---- .../services/key-generation.service.ts | 18 +++++++++++++++--- libs/key-management/src/key.service.spec.ts | 4 ++-- libs/key-management/src/key.service.ts | 13 +++++++++---- 15 files changed, 58 insertions(+), 45 deletions(-) diff --git a/apps/cli/src/admin-console/commands/confirm.command.ts b/apps/cli/src/admin-console/commands/confirm.command.ts index 0d5c7ba069c..1c900511499 100644 --- a/apps/cli/src/admin-console/commands/confirm.command.ts +++ b/apps/cli/src/admin-console/commands/confirm.command.ts @@ -57,7 +57,7 @@ export class ConfirmCommand { } const publicKeyResponse = await this.apiService.getUserPublicKey(orgUser.userId); const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - const key = await this.encryptService.rsaEncrypt(orgKey.key, publicKey); + const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey); const req = new OrganizationUserConfirmRequest(); req.key = key.encryptedString; await this.organizationUserApiService.postOrganizationUserConfirm( diff --git a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts index 4c8d483a0c5..f262ba1abd0 100644 --- a/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts +++ b/bitwarden_license/bit-web/src/app/billing/providers/setup/setup-business-unit.component.ts @@ -84,10 +84,8 @@ export class SetupBusinessUnitComponent extends BaseAcceptComponent { const organizationKey = await firstValueFrom(organizationKey$); - const { encryptedString: encryptedOrganizationKey } = await this.encryptService.encrypt( - organizationKey.key, - providerKey, - ); + const { encryptedString: encryptedOrganizationKey } = + await this.encryptService.wrapSymmetricKey(organizationKey, providerKey); if (!encryptedProviderKey || !encryptedOrganizationKey) { return await fail(); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index f7bf2260a36..0d2df969f87 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -123,7 +123,9 @@ describe("AuthRequestService", () => { }); it("should use the user key if the master key and hash do not exist", async () => { - keyService.getUserKey.mockResolvedValueOnce({ key: new Uint8Array(64) } as UserKey); + keyService.getUserKey.mockResolvedValueOnce( + new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, + ); await sut.approveOrDenyAuthRequest( true, @@ -131,7 +133,7 @@ describe("AuthRequestService", () => { ); expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith( - { key: new Uint8Array(64) }, + new SymmetricCryptoKey(new Uint8Array(64)), expect.anything(), ); }); diff --git a/libs/auth/src/common/services/pin/pin.service.spec.ts b/libs/auth/src/common/services/pin/pin.service.spec.ts index fd33f5d2077..5469b121f10 100644 --- a/libs/auth/src/common/services/pin/pin.service.spec.ts +++ b/libs/auth/src/common/services/pin/pin.service.spec.ts @@ -434,7 +434,7 @@ describe("PinService", () => { .fn() .mockResolvedValue(pinKeyEncryptedUserKeyPersistant); sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey); - encryptService.decryptToBytes.mockResolvedValue(mockUserKey.key); + encryptService.decryptToBytes.mockResolvedValue(mockUserKey.toEncoded()); } function mockPinEncryptedKeyDataByPinLockType(pinLockType: PinLockType) { diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.spec.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.spec.ts index 614c593048d..eca1f188d85 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.spec.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.spec.ts @@ -21,7 +21,7 @@ describe("WebAuthnLoginPrfKeyService", () => { const result = await service.createSymmetricKeyFromPrf(randomBytes(32)); - expect(result.key.length).toBe(64); + expect(result.toEncoded().length).toBe(64); }); }); }); diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts index 0b6a6b641ae..addcc978c23 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.implementation.ts @@ -47,7 +47,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64) { throw new Error("Type 0 encryption is not supported."); } } @@ -105,7 +105,7 @@ export class EncryptServiceImplementation implements EncryptService { throw new Error("No wrappingKey provided for wrapping."); } - return await this.encryptUint8Array(keyToBeWrapped.key, wrappingKey); + return await this.encryptUint8Array(keyToBeWrapped.toEncoded(), wrappingKey); } private async encryptUint8Array( @@ -147,7 +147,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (this.blockType0) { - if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) { + if (key.inner().type === EncryptionType.AesCbc256_B64) { throw new Error("Type 0 encryption is not supported."); } } diff --git a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts index 69a628f9604..dd0b909f7fb 100644 --- a/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts +++ b/libs/common/src/key-management/crypto/services/encrypt.service.spec.ts @@ -184,10 +184,9 @@ describe("EncryptService", () => { (encryptService as any).blockType0 = true; const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); - mock32Key.key = makeStaticByteArray(32); mock32Key.inner.mockReturnValue({ type: 0, - encryptionKey: mock32Key.key, + encryptionKey: makeStaticByteArray(32), }); await expect(encryptService.encrypt(null!, key)).rejects.toThrow( @@ -270,10 +269,9 @@ describe("EncryptService", () => { (encryptService as any).blockType0 = true; const key = new SymmetricCryptoKey(makeStaticByteArray(32)); const mock32Key = mock(); - mock32Key.key = makeStaticByteArray(32); mock32Key.inner.mockReturnValue({ type: 0, - encryptionKey: mock32Key.key, + encryptionKey: makeStaticByteArray(32), }); await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow( @@ -571,7 +569,7 @@ describe("EncryptService", () => { const actual = await encryptService.encapsulateKeyUnsigned(testKey, publicKey); expect(cryptoFunctionService.rsaEncrypt).toBeCalledWith( - expect.toEqualBuffer(testKey.key), + expect.toEqualBuffer(testKey.toEncoded()), expect.toEqualBuffer(publicKey), "sha1", ); @@ -622,7 +620,7 @@ describe("EncryptService", () => { "sha1", ); - expect(actual.key).toEqualBuffer(data); + expect(actual.toEncoded()).toEqualBuffer(data); }); }); }); 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 205f332d0f9..06999cab9c3 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 @@ -221,8 +221,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { } const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey); - const newEncryptedUserKey = await this.encryptService.rsaEncrypt( - newUserKey.key, + const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned( + newUserKey, publicKey, ); 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 e78fc01b694..7d4a25c1f08 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 @@ -450,7 +450,7 @@ describe("deviceTrustService", () => { // RsaEncrypt must be called w/ a user key array buffer of 64 bytes const userKey = cryptoSvcRsaEncryptSpy.mock.calls[0][0]; - expect(userKey.key.byteLength).toBe(64); + expect(userKey.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); expect(encryptServiceWrapDecapsulationKeySpy).toHaveBeenCalledTimes(1); expect(encryptServiceWrapEncapsulationKeySpy).toHaveBeenCalledTimes(1); @@ -706,7 +706,9 @@ describe("deviceTrustService", () => { ); encryptService.decryptToBytes.mockResolvedValue(null); encryptService.encrypt.mockResolvedValue(new EncString("test_encrypted_data")); - encryptService.rsaEncrypt.mockResolvedValue(new EncString("test_encrypted_data")); + encryptService.encapsulateKeyUnsigned.mockResolvedValue( + new EncString("test_encrypted_data"), + ); const protectedDeviceResponse = new ProtectedDeviceResponse({ id: "id", @@ -861,8 +863,8 @@ describe("deviceTrustService", () => { // Mock the decryption of the public key with the old user key encryptService.decryptToBytes.mockImplementationOnce((_encValue, privateKeyValue) => { - expect(privateKeyValue.key.byteLength).toBe(64); - expect(new Uint8Array(privateKeyValue.key)[0]).toBe(FakeOldUserKeyMarker); + expect(privateKeyValue.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); + expect(new Uint8Array(privateKeyValue.toEncoded())[0]).toBe(FakeOldUserKeyMarker); const data = new Uint8Array(250); data.fill(FakeDecryptedPublicKeyMarker, 0, 1); return Promise.resolve(data); @@ -870,8 +872,8 @@ describe("deviceTrustService", () => { // Mock the encryption of the new user key with the decrypted public key encryptService.encapsulateKeyUnsigned.mockImplementationOnce((data, publicKey) => { - expect(data.key.byteLength).toBe(64); // New key should also be 64 bytes - expect(new Uint8Array(data.key)[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1'; + expect(data.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64); // New key should also be 64 bytes + expect(new Uint8Array(data.toEncoded())[0]).toBe(FakeNewUserKeyMarker); // New key should have the first byte be '1'; expect(new Uint8Array(publicKey)[0]).toBe(FakeDecryptedPublicKeyMarker); return Promise.resolve(new EncString("4.ZW5jcnlwdGVkdXNlcg==")); @@ -882,7 +884,7 @@ describe("deviceTrustService", () => { expect(plainValue).toBeInstanceOf(Uint8Array); expect(new Uint8Array(plainValue as Uint8Array)[0]).toBe(FakeDecryptedPublicKeyMarker); - expect(new Uint8Array(key.key)[0]).toBe(FakeNewUserKeyMarker); + expect(new Uint8Array(key.toEncoded())[0]).toBe(FakeNewUserKeyMarker); return Promise.resolve( new EncString("2.ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj|ZW5jcnlwdGVkcHVibGlj"), ); diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts index 6b641ad443a..9246652b4c8 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.spec.ts @@ -19,7 +19,6 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", innerKey: { type: EncryptionType.AesCbc256_B64, @@ -33,7 +32,6 @@ describe("SymmetricCryptoKey", () => { const cryptoKey = new SymmetricCryptoKey(key); expect(cryptoKey).toEqual({ - key: key, keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==", innerKey: { diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index c85f3432b28..ad16ddd06f6 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -24,7 +24,6 @@ export type Aes256CbcKey = { export class SymmetricCryptoKey { private innerKey: Aes256CbcHmacKey | Aes256CbcKey; - key: Uint8Array; keyB64: string; /** @@ -40,7 +39,6 @@ export class SymmetricCryptoKey { type: EncryptionType.AesCbc256_B64, encryptionKey: key, }; - this.key = key; this.keyB64 = this.toBase64(); } else if (key.byteLength === 64) { this.innerKey = { @@ -48,7 +46,6 @@ export class SymmetricCryptoKey { encryptionKey: key.slice(0, 32), authenticationKey: key.slice(32), }; - this.key = key; this.keyB64 = this.toBase64(); } else { throw new Error(`Unsupported encType/key length ${key.byteLength}`); diff --git a/libs/common/src/platform/services/key-generation.service.spec.ts b/libs/common/src/platform/services/key-generation.service.spec.ts index a2597414d0e..f75eaeb25be 100644 --- a/libs/common/src/platform/services/key-generation.service.spec.ts +++ b/libs/common/src/platform/services/key-generation.service.spec.ts @@ -4,6 +4,7 @@ import { PBKDF2KdfConfig, Argon2KdfConfig } from "@bitwarden/key-management"; import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service"; import { CsprngArray } from "../../types/csprng"; +import { EncryptionType } from "../enums"; import { KeyGenerationService } from "./key-generation.service"; @@ -52,7 +53,7 @@ describe("KeyGenerationService", () => { expect(salt).toEqual(inputSalt); expect(material).toEqual(inputMaterial); - expect(derivedKey.key.length).toEqual(64); + expect(derivedKey.inner().type).toEqual(EncryptionType.AesCbc256_HmacSha256_B64); }, ); }); @@ -67,7 +68,7 @@ describe("KeyGenerationService", () => { const key = await sut.deriveKeyFromMaterial(material, salt, purpose); - expect(key.key.length).toEqual(64); + expect(key.inner().type).toEqual(EncryptionType.AesCbc256_HmacSha256_B64); }); }); @@ -81,7 +82,7 @@ describe("KeyGenerationService", () => { const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); - expect(key.key.length).toEqual(32); + expect(key.inner().type).toEqual(EncryptionType.AesCbc256_B64); }); it("should derive a 32 byte key from a password using argon2id", async () => { @@ -94,7 +95,7 @@ describe("KeyGenerationService", () => { const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); - expect(key.key.length).toEqual(32); + expect(key.inner().type).toEqual(EncryptionType.AesCbc256_B64); }); }); }); diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts index 6203eaabdd1..8f9e6856aa0 100644 --- a/libs/common/src/platform/services/key-generation.service.ts +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { MasterKey, PinKey } from "@bitwarden/common/types/key"; import { KdfConfig, PBKDF2KdfConfig, Argon2KdfConfig, KdfType } from "@bitwarden/key-management"; import { CryptoFunctionService } from "../../key-management/crypto/abstractions/crypto-function.service"; @@ -78,10 +79,21 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction { return new SymmetricCryptoKey(key); } - async stretchKey(key: SymmetricCryptoKey): Promise { + async stretchKey(key: MasterKey | PinKey): Promise { const newKey = new Uint8Array(64); - const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256"); - const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256"); + // Master key and pin key are always 32 bytes + const encKey = await this.cryptoFunctionService.hkdfExpand( + key.inner().encryptionKey, + "enc", + 32, + "sha256", + ); + const macKey = await this.cryptoFunctionService.hkdfExpand( + key.inner().encryptionKey, + "mac", + 32, + "sha256", + ); newKey.set(new Uint8Array(encKey)); newKey.set(new Uint8Array(macKey), 32); diff --git a/libs/key-management/src/key.service.spec.ts b/libs/key-management/src/key.service.spec.ts index 90d049b7293..57291462924 100644 --- a/libs/key-management/src/key.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -497,7 +497,7 @@ describe("keyService", () => { const output = new Uint8Array(64); output.set(encryptedPrivateKey.dataBytes); output.set( - key.key.subarray(0, 64 - encryptedPrivateKey.dataBytes.length), + key.toEncoded().subarray(0, 64 - encryptedPrivateKey.dataBytes.length), encryptedPrivateKey.dataBytes.length, ); return output; @@ -827,7 +827,7 @@ describe("keyService", () => { masterPasswordService.masterKeyHashSubject.next(storedMasterKeyHash); cryptoFunctionService.pbkdf2 - .calledWith(masterKey.key, masterPassword as string, "sha256", 2) + .calledWith(masterKey.inner().encryptionKey, masterPassword as string, "sha256", 2) .mockResolvedValue(Utils.fromB64ToArray(mockReturnedHash)); const actualDidMatch = await keyService.compareKeyHash( diff --git a/libs/key-management/src/key.service.ts b/libs/key-management/src/key.service.ts index 5dec206b86b..a3b6a1fa2d1 100644 --- a/libs/key-management/src/key.service.ts +++ b/libs/key-management/src/key.service.ts @@ -26,7 +26,7 @@ import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/ke import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums"; +import { KeySuffixOptions, HashPurpose, EncryptionType } from "@bitwarden/common/platform/enums"; import { convertValues } from "@bitwarden/common/platform/misc/convert-values"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EFFLongWordList } from "@bitwarden/common/platform/misc/wordlist"; @@ -346,7 +346,12 @@ export class DefaultKeyService implements KeyServiceAbstraction { } const iterations = hashPurpose === HashPurpose.LocalAuthorization ? 2 : 1; - const hash = await this.cryptoFunctionService.pbkdf2(key.key, password, "sha256", iterations); + const hash = await this.cryptoFunctionService.pbkdf2( + key.inner().encryptionKey, + password, + "sha256", + iterations, + ); return Utils.fromBufferToB64(hash); } @@ -823,13 +828,13 @@ export class DefaultKeyService implements KeyServiceAbstraction { newSymKey: SymmetricCryptoKey, ): Promise<[T, EncString]> { let protectedSymKey: EncString; - if (encryptionKey.key.byteLength === 32) { + if (encryptionKey.inner().type === EncryptionType.AesCbc256_B64) { const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey); protectedSymKey = await this.encryptService.wrapSymmetricKey( newSymKey, stretchedEncryptionKey, ); - } else if (encryptionKey.key.byteLength === 64) { + } else if (encryptionKey.inner().type === EncryptionType.AesCbc256_HmacSha256_B64) { protectedSymKey = await this.encryptService.wrapSymmetricKey(newSymKey, encryptionKey); } else { throw new Error("Invalid key size."); From 24786a08df6e059c5f9f3e4e7fa7abde54e4247c Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Fri, 25 Apr 2025 14:42:11 -0400 Subject: [PATCH 08/14] [PM-20451] Refactor Windows PA Types (#14366) * PM-20451: Rename and organize Windows types * PM-20451: Add comments --- .../windows_plugin_authenticator/src/lib.rs | 167 +++--------------- .../src/pluginauthenticator.rs | 110 ++++++++++++ .../src/webauthn.rs | 29 +++ 3 files changed, 161 insertions(+), 145 deletions(-) create mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/src/pluginauthenticator.rs create mode 100644 apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs index 21257068bd0..cdea50aee99 100644 --- a/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/lib.rs @@ -9,58 +9,14 @@ use windows::Win32::System::Com::*; use windows::Win32::System::LibraryLoader::*; use windows_core::*; +mod pluginauthenticator; +mod webauthn; + const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator"; //const AAGUID: &str = "d548826e-79b4-db40-a3d8-11116f7e8349"; const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062"; const RPID: &str = "bitwarden.com"; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST { - pub transactionId: GUID, - pub cbRequestSignature: Dword, - pub pbRequestSignature: *mut byte, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST { - pub hWnd: HWND, - pub transactionId: GUID, - pub cbRequestSignature: Dword, - pub pbRequestSignature: *mut byte, - pub cbEncodedRequest: Dword, - pub pbEncodedRequest: *mut byte, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { - pub cbOpSignPubKey: Dword, - pub pbOpSignPubKey: PByte, -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE { - pub cbEncodedResponse: Dword, - pub pbEncodedResponse: *mut byte, -} - -type Dword = u32; -type Byte = u8; -type byte = u8; -pub type PByte = *mut Byte; - -type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST = - *const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST; -pub type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST = - *const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST; -pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE = - *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE; -pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE = - *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE; - /// Handles initialization and registration for the Bitwarden desktop app as a /// plugin authenticator with Windows. /// For now, also adds the authenticator @@ -109,7 +65,8 @@ fn initialize_com_library() -> std::result::Result<(), String> { /// Registers the Bitwarden Plugin Authenticator COM library with Windows. fn register_com_library() -> std::result::Result<(), String> { - static FACTORY: windows_core::StaticComObject = Factory().into_static(); + static FACTORY: windows_core::StaticComObject = + pluginauthenticator::Factory().into_static(); let clsid: *const GUID = &GUID::from_u128(0xa98925d161f640de9327dc418fcb2ff4); match unsafe { @@ -146,25 +103,25 @@ fn add_authenticator() -> std::result::Result<(), String> { let cbor_authenticator_info = "A60182684649444F5F325F30684649444F5F325F310282637072666B686D61632D7365637265740350D548826E79B4DB40A3D811116F7E834904A362726BF5627570F5627576F5098168696E7465726E616C0A81A263616C672664747970656A7075626C69632D6B6579"; let mut authenticator_info_bytes = hex::decode(cbor_authenticator_info).unwrap(); - let add_authenticator_options = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS { - pwszAuthenticatorName: authenticator_name_ptr, - pwszPluginClsId: clsid_ptr, - pwszPluginRpId: relying_party_id_ptr, - pwszLightThemeLogo: ptr::null(), // unused by Windows - pwszDarkThemeLogo: ptr::null(), // unused by Windows - cbAuthenticatorInfo: authenticator_info_bytes.len() as u32, - pbAuthenticatorInfo: authenticator_info_bytes.as_mut_ptr(), + let add_authenticator_options = webauthn::ExperimentalWebAuthnPluginAddAuthenticatorOptions { + authenticator_name: authenticator_name_ptr, + com_clsid: clsid_ptr, + rpid: relying_party_id_ptr, + light_theme_logo: ptr::null(), // unused by Windows + dark_theme_logo: ptr::null(), // unused by Windows + cbor_authenticator_info_byte_count: authenticator_info_bytes.len() as u32, + cbor_authenticator_info: authenticator_info_bytes.as_mut_ptr(), }; - let plugin_signing_public_key_byte_count: Dword = 0; + let plugin_signing_public_key_byte_count: u32 = 0; let mut plugin_signing_public_key: c_uchar = 0; - let plugin_signing_public_key_ptr: PByte = &mut plugin_signing_public_key; + let plugin_signing_public_key_ptr = &mut plugin_signing_public_key; - let mut add_response = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE { - cbOpSignPubKey: plugin_signing_public_key_byte_count, - pbOpSignPubKey: plugin_signing_public_key_ptr, + let mut add_response = webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse { + plugin_operation_signing_key_byte_count: plugin_signing_public_key_byte_count, + plugin_operation_signing_key: plugin_signing_public_key_ptr, }; - let mut add_response_ptr: *mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE = + let mut add_response_ptr: *mut webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse = &mut add_response; let result = unsafe { @@ -193,23 +150,10 @@ fn add_authenticator() -> std::result::Result<(), String> { } } -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS { - pub pwszAuthenticatorName: *const u16, - pub pwszPluginClsId: *const u16, - pub pwszPluginRpId: *const u16, - pub pwszLightThemeLogo: *const u16, - pub pwszDarkThemeLogo: *const u16, - pub cbAuthenticatorInfo: u32, - pub pbAuthenticatorInfo: *const u8, -} - type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn( - pPluginAddAuthenticatorOptions: *const EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS, - ppPluginAddAuthenticatorResponse: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, -) - -> HRESULT; + pPluginAddAuthenticatorOptions: *const webauthn::ExperimentalWebAuthnPluginAddAuthenticatorOptions, + ppPluginAddAuthenticatorResponse: *mut *mut webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse, +) -> HRESULT; unsafe fn delay_load(library: PCSTR, function: PCSTR) -> Option { let library = LoadLibraryExA(library, None, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); @@ -228,70 +172,3 @@ unsafe fn delay_load(library: PCSTR, function: PCSTR) -> Option { None } - -#[interface("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")] -unsafe trait EXPERIMENTAL_IPluginAuthenticator: IUnknown { - fn EXPERIMENTAL_PluginMakeCredential( - &self, - request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST, - response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE, - ) -> HRESULT; - fn EXPERIMENTAL_PluginGetAssertion( - &self, - request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST, - response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE, - ) -> HRESULT; - fn EXPERIMENTAL_PluginCancelOperation( - &self, - request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, - ) -> HRESULT; -} - -#[implement(EXPERIMENTAL_IPluginAuthenticator)] -struct PACOMObject; - -impl EXPERIMENTAL_IPluginAuthenticator_Impl for PACOMObject_Impl { - unsafe fn EXPERIMENTAL_PluginMakeCredential( - &self, - _request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST, - _response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE, - ) -> HRESULT { - HRESULT(0) - } - - unsafe fn EXPERIMENTAL_PluginGetAssertion( - &self, - _request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST, - _response: *mut EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE, - ) -> HRESULT { - HRESULT(0) - } - - unsafe fn EXPERIMENTAL_PluginCancelOperation( - &self, - _request: EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST, - ) -> HRESULT { - HRESULT(0) - } -} - -#[implement(IClassFactory)] -struct Factory(); - -impl IClassFactory_Impl for Factory_Impl { - fn CreateInstance( - &self, - outer: Ref, - iid: *const GUID, - object: *mut *mut core::ffi::c_void, - ) -> Result<()> { - assert!(outer.is_null()); - let unknown: IInspectable = PACOMObject.into(); - unsafe { unknown.query(iid, object).ok() } - } - - fn LockServer(&self, lock: BOOL) -> Result<()> { - assert!(lock.as_bool()); - Ok(()) - } -} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/pluginauthenticator.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/pluginauthenticator.rs new file mode 100644 index 00000000000..132f9effcde --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/pluginauthenticator.rs @@ -0,0 +1,110 @@ +/* + This file exposes the functions and types defined here: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h +*/ + +use windows::Win32::System::Com::*; +use windows_core::*; + +/// Used when creating and asserting credentials. +/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST +/// Header File Usage: EXPERIMENTAL_PluginMakeCredential() +/// EXPERIMENTAL_PluginGetAssertion() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ExperimentalWebAuthnPluginOperationRequest { + pub window_handle: windows::Win32::Foundation::HWND, + pub transaction_id: windows_core::GUID, + pub request_signature_byte_count: u32, + pub request_signature_pointer: *mut u8, + pub encoded_request_byte_count: u32, + pub encoded_request_pointer: *mut u8, +} + +/// Used as a response when creating and asserting credentials. +/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE +/// Header File Usage: EXPERIMENTAL_PluginMakeCredential() +/// EXPERIMENTAL_PluginGetAssertion() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ExperimentalWebAuthnPluginOperationResponse { + pub encoded_response_byte_count: u32, + pub encoded_response_pointer: *mut u8, +} + +/// Used to cancel an operation. +/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST +/// Header File Usage: EXPERIMENTAL_PluginCancelOperation() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ExperimentalWebAuthnPluginCancelOperationRequest { + pub transaction_id: windows_core::GUID, + pub request_signature_byte_count: u32, + pub request_signature_pointer: *mut u8, +} + +#[interface("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")] +pub unsafe trait EXPERIMENTAL_IPluginAuthenticator: IUnknown { + fn EXPERIMENTAL_PluginMakeCredential( + &self, + request: *const ExperimentalWebAuthnPluginOperationRequest, + response: *mut ExperimentalWebAuthnPluginOperationResponse, + ) -> HRESULT; + fn EXPERIMENTAL_PluginGetAssertion( + &self, + request: *const ExperimentalWebAuthnPluginOperationRequest, + response: *mut ExperimentalWebAuthnPluginOperationResponse, + ) -> HRESULT; + fn EXPERIMENTAL_PluginCancelOperation( + &self, + request: *const ExperimentalWebAuthnPluginCancelOperationRequest, + ) -> HRESULT; +} + +#[implement(EXPERIMENTAL_IPluginAuthenticator)] +pub struct PluginAuthenticatorComObject; + +#[implement(IClassFactory)] +pub struct Factory(); + +impl EXPERIMENTAL_IPluginAuthenticator_Impl for PluginAuthenticatorComObject_Impl { + unsafe fn EXPERIMENTAL_PluginMakeCredential( + &self, + _request: *const ExperimentalWebAuthnPluginOperationRequest, + _response: *mut ExperimentalWebAuthnPluginOperationResponse, + ) -> HRESULT { + HRESULT(0) + } + + unsafe fn EXPERIMENTAL_PluginGetAssertion( + &self, + _request: *const ExperimentalWebAuthnPluginOperationRequest, + _response: *mut ExperimentalWebAuthnPluginOperationResponse, + ) -> HRESULT { + HRESULT(0) + } + + unsafe fn EXPERIMENTAL_PluginCancelOperation( + &self, + _request: *const ExperimentalWebAuthnPluginCancelOperationRequest, + ) -> HRESULT { + HRESULT(0) + } +} + +impl IClassFactory_Impl for Factory_Impl { + fn CreateInstance( + &self, + outer: Ref, + iid: *const GUID, + object: *mut *mut core::ffi::c_void, + ) -> Result<()> { + assert!(outer.is_null()); + let unknown: IInspectable = PluginAuthenticatorComObject.into(); + unsafe { unknown.query(iid, object).ok() } + } + + fn LockServer(&self, lock: BOOL) -> Result<()> { + assert!(lock.as_bool()); + Ok(()) + } +} diff --git a/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs b/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs new file mode 100644 index 00000000000..18c7563ffd8 --- /dev/null +++ b/apps/desktop/desktop_native/windows_plugin_authenticator/src/webauthn.rs @@ -0,0 +1,29 @@ +/* + This file exposes the functions and types defined here: https://github.com/microsoft/webauthn/blob/master/experimental/webauthn.h +*/ + +/// Used when adding a Windows plugin authenticator. +/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_OPTIONS +/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ExperimentalWebAuthnPluginAddAuthenticatorOptions { + pub authenticator_name: *const u16, + pub com_clsid: *const u16, + pub rpid: *const u16, + pub light_theme_logo: *const u16, + pub dark_theme_logo: *const u16, + pub cbor_authenticator_info_byte_count: u32, + pub cbor_authenticator_info: *const u8, +} + +/// Used as a response type when adding a Windows plugin authenticator. +/// Header File Name: _EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE +/// Header File Usage: EXPERIMENTAL_WebAuthNPluginAddAuthenticator() +/// EXPERIMENTAL_WebAuthNPluginFreeAddAuthenticatorResponse() +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct ExperimentalWebAuthnPluginAddAuthenticatorResponse { + pub plugin_operation_signing_key_byte_count: u32, + pub plugin_operation_signing_key: *mut u8, +} From 74d01eca81923bee58dd9311b5a3fb9d0ecdaa1c Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Mon, 28 Apr 2025 04:14:09 -0400 Subject: [PATCH 09/14] [BRE-784] Fixing web vault build to pull valid server ref (#14382) --- .github/workflows/build-web.yml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index fb429468134..4ca6dc25aab 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -133,12 +133,34 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} + - name: Get Latest Server Version + id: latest-server-version + uses: bitwarden/gh-actions/get-release-version@main + with: + repository: bitwarden/server + trim: false + + - name: Set Server Ref + id: set-server-ref + run: | + SERVER_REF="${{ steps.latest-server-version.outputs.version }}" + echo "Latest server release version: $SERVER_REF" + if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then + SERVER_REF="$GITHUB_REF" + elif [[ "$GITHUB_REF" == "refs/heads/rc" ]]; then + SERVER_REF="$GITHUB_REF" + elif [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + SERVER_REF="refs/heads/main" + fi + echo "Server ref: $SERVER_REF" + echo "server_ref=$SERVER_REF" >> $GITHUB_OUTPUT + - name: Check out Server repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: path: server repository: bitwarden/server - ref: ${{ github.event.pull_request.head.sha && 'main' || github.ref }} + ref: ${{ steps.set-server-ref.outputs.server_ref }} - name: Check Branch to Publish env: @@ -160,7 +182,7 @@ jobs: VERSION=$( jq -r ".version" package.json) jq --arg version "$VERSION+${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp mv package.json.tmp package.json - + ########## Set up Docker ########## - name: Set up Docker uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 # v4.3.0 @@ -304,7 +326,7 @@ jobs: - name: Log out of Docker run: docker logout $_AZ_REGISTRY - + crowdin-push: name: Crowdin Push if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' From 5fc4e32b8f9d1bd6a46df4b37dd798100acb2067 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:44:43 +0200 Subject: [PATCH 10/14] [deps] Platform: Update Rust crate typenum to v1.18.0 (#14486) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index c786264a563..de04b6ede2f 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -3022,9 +3022,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uds_windows" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index dc08c2e5a1b..ed943c0021b 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -54,7 +54,7 @@ thiserror = "=1.0.69" tokio = "=1.43.1" tokio-stream = "=0.1.15" tokio-util = "=0.7.13" -typenum = "=1.17.0" +typenum = "=1.18.0" uniffi = "=0.28.3" widestring = "=1.1.0" windows = "=0.61.1" From 434852b9b927d8147cdc97aa99733108d552354d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:45:26 +0200 Subject: [PATCH 11/14] [deps] Platform: Update Rust crate pin-project to v1.1.10 (#14458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- apps/desktop/desktop_native/Cargo.lock | 8 ++++---- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index de04b6ede2f..bf7fd9dcc66 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -2110,18 +2110,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index ed943c0021b..e6022d2c347 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -35,7 +35,7 @@ napi-build = "=2.1.4" napi-derive = "=2.16.13" oo7 = "=0.3.3" oslog = "=0.2.0" -pin-project = "=1.1.8" +pin-project = "=1.1.10" pkcs8 = "=0.10.2" rand = "=0.8.5" rsa = "=0.9.8" From 210f26dbe4fddbf8b53c44e0b4925c2f1d793f3d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 12:17:19 +0200 Subject: [PATCH 12/14] [deps] Platform: Update @electron/rebuild to v3.7.2 (#14453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel García --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e08a6df9a8..5df7882a89b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,7 @@ "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", "@electron/notarize": "2.5.0", - "@electron/rebuild": "3.7.1", + "@electron/rebuild": "3.7.2", "@lit-labs/signals": "0.1.2", "@ngtools/webpack": "18.2.12", "@storybook/addon-a11y": "8.5.2", @@ -5830,9 +5830,9 @@ } }, "node_modules/@electron/rebuild": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.1.tgz", - "integrity": "sha512-sKGD+xav4Gh25+LcLY0rjIwcCFTw+f/HU1pB48UVbwxXXRGaXEqIH0AaYKN46dgd/7+6kuiDXzoyAEvx1zCsdw==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz", + "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 861559b2b66..ddace570681 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", "@electron/notarize": "2.5.0", - "@electron/rebuild": "3.7.1", + "@electron/rebuild": "3.7.2", "@lit-labs/signals": "0.1.2", "@ngtools/webpack": "18.2.12", "@storybook/addon-a11y": "8.5.2", From dff58de619482beaa7ef6f22f9c43d6996d5524d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:39:57 +0200 Subject: [PATCH 13/14] [deps] Architecture: Update Linting minor-patch (#14478) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 727 +++++++++++++++++++++++++++++++--------------- package.json | 10 +- 2 files changed, 498 insertions(+), 239 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5df7882a89b..23ed852b06a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -138,12 +138,12 @@ "electron-store": "8.2.0", "electron-updater": "6.3.9", "eslint": "8.57.1", - "eslint-config-prettier": "10.0.1", - "eslint-import-resolver-typescript": "3.7.0", + "eslint-config-prettier": "10.1.2", + "eslint-import-resolver-typescript": "3.10.1", "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", - "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-storybook": "0.12.0", "eslint-plugin-tailwindcss": "3.18.0", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", @@ -154,7 +154,7 @@ "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", "json5": "2.2.3", - "lint-staged": "15.4.1", + "lint-staged": "15.5.1", "mini-css-extract-plugin": "2.9.2", "nx": "20.8.0", "postcss": "8.5.1", @@ -174,7 +174,7 @@ "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", - "typescript-eslint": "8.20.0", + "typescript-eslint": "8.30.1", "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", @@ -11961,21 +11961,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", - "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", + "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/type-utils": "8.20.0", - "@typescript-eslint/utils": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/type-utils": "8.30.1", + "@typescript-eslint/utils": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -11987,18 +11987,18 @@ "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", - "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12008,35 +12008,21 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", - "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", - "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12046,20 +12032,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", - "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0" + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12070,17 +12056,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", - "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -12305,16 +12291,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", - "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", + "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4" }, "engines": { @@ -12326,18 +12312,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", - "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12347,35 +12333,21 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", - "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", - "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12385,17 +12357,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", - "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -12501,16 +12473,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", - "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", + "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.20.0", - "@typescript-eslint/utils": "8.20.0", + "@typescript-eslint/typescript-estree": "8.30.1", + "@typescript-eslint/utils": "8.30.1", "debug": "^4.3.4", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12521,18 +12493,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", - "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12542,35 +12514,21 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", - "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", - "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12580,20 +12538,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", - "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0" + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12604,17 +12562,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", - "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -12639,9 +12597,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", - "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", + "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "dev": true, "license": "MIT", "engines": { @@ -12783,6 +12741,260 @@ "dev": true, "license": "ISC" }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", + "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", + "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", + "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", + "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", + "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", + "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", + "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", + "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", + "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", + "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", + "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", + "integrity": "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz", + "integrity": "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", + "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.9" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", + "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", + "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", + "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", + "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vitejs/plugin-basic-ssl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", @@ -18790,13 +19002,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz", + "integrity": "sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==", "dev": true, "license": "MIT", "bin": { - "eslint-config-prettier": "build/bin/cli.js" + "eslint-config-prettier": "bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -18841,26 +19053,25 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", - "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, "license": "ISC", "dependencies": { "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.3.7", - "enhanced-resolve": "^5.15.0", - "fast-glob": "^3.3.2", - "get-tsconfig": "^4.7.5", - "is-bun-module": "^1.0.2", - "is-glob": "^4.0.3", - "stable-hash": "^0.0.4" + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + "url": "https://opencollective.com/eslint-import-resolver-typescript" }, "peerDependencies": { "eslint": "*", @@ -19036,9 +19247,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.11.2.tgz", - "integrity": "sha512-0Z4DUklJrC+GHjCRXa7PYfPzWC15DaVnwaOYenpgXiCEijXPZkLKCms+rHhtoRcWccP7Z8DpOOaP1gc3P9oOwg==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.12.0.tgz", + "integrity": "sha512-Lg5I0+npTgiYgZ4KSvGWGDFZi3eOCNJPaWX0c9rTEEXC5wvooOClsP9ZtbI4hhFKyKgYR877KiJxbRTSJq9gWA==", "dev": true, "license": "MIT", "dependencies": { @@ -19849,9 +20060,9 @@ } }, "node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -22243,13 +22454,26 @@ } }, "node_modules/is-bun-module": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", - "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.6.3" + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/is-callable": { @@ -25159,22 +25383,22 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.1.tgz", - "integrity": "sha512-P8yJuVRyLrm5KxCtFx+gjI5Bil+wO7wnTl7C3bXhvtTaAFGirzeB24++D0wGoUwxrUKecNiehemgCob9YL39NA==", + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz", + "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "~5.4.1", - "commander": "~12.1.0", - "debug": "~4.4.0", - "execa": "~8.0.1", - "lilconfig": "~3.1.3", - "listr2": "~8.2.5", - "micromatch": "~4.0.8", - "pidtree": "~0.6.0", - "string-argv": "~0.3.2", - "yaml": "~2.6.1" + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -25243,9 +25467,9 @@ } }, "node_modules/lint-staged/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, "license": "MIT", "engines": { @@ -25340,9 +25564,9 @@ } }, "node_modules/lint-staged/node_modules/listr2": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", - "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.2.tgz", + "integrity": "sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==", "dev": true, "license": "MIT", "dependencies": { @@ -27838,6 +28062,22 @@ "dev": true, "license": "MIT" }, + "node_modules/napi-postinstall": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.2.tgz", + "integrity": "sha512-Wy1VI/hpKHwy1MsnFxHCJxqFwmmxD0RA/EKPL7e6mfbsY01phM2SZyJnRdU0bLvhu0Quby1DCcAZti3ghdl4/A==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -33213,9 +33453,9 @@ "license": "ISC" }, "node_modules/stable-hash": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", - "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", "dev": true, "license": "MIT" }, @@ -34292,13 +34532,13 @@ "peer": true }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -35160,15 +35400,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.20.0.tgz", - "integrity": "sha512-Kxz2QRFsgbWj6Xcftlw3Dd154b3cEPFqQC+qMZrMypSijPd4UanKKvoKDrJ4o8AIfZFKAF+7sMaEIR8mTElozA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.30.1.tgz", + "integrity": "sha512-D7lC0kcehVH7Mb26MRQi64LMyRJsj3dToJxM1+JVTl53DQSV5/7oUGWQLcKl1C1KnoVHxMMU2FNQMffr7F3Row==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.20.0", - "@typescript-eslint/parser": "8.20.0", - "@typescript-eslint/utils": "8.20.0" + "@typescript-eslint/eslint-plugin": "8.30.1", + "@typescript-eslint/parser": "8.30.1", + "@typescript-eslint/utils": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -35179,18 +35419,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", - "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", + "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0" + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -35200,35 +35440,21 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", - "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", - "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", + "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/visitor-keys": "8.20.0", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/visitor-keys": "8.30.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.0" + "ts-api-utils": "^2.0.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -35238,20 +35464,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", - "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", + "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.20.0", - "@typescript-eslint/types": "8.20.0", - "@typescript-eslint/typescript-estree": "8.20.0" + "@typescript-eslint/scope-manager": "8.30.1", + "@typescript-eslint/types": "8.30.1", + "@typescript-eslint/typescript-estree": "8.30.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -35262,17 +35488,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.8.0" + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", - "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", + "version": "8.30.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", + "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/types": "8.30.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -35669,6 +35895,39 @@ "node": ">=14.0.0" } }, + "node_modules/unrs-resolver": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", + "integrity": "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/JounQin" + }, + "optionalDependencies": { + "@unrs/resolver-binding-darwin-arm64": "1.7.2", + "@unrs/resolver-binding-darwin-x64": "1.7.2", + "@unrs/resolver-binding-freebsd-x64": "1.7.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-x64-musl": "1.7.2", + "@unrs/resolver-binding-wasm32-wasi": "1.7.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -37722,9 +37981,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index ddace570681..02d81c5fddc 100644 --- a/package.json +++ b/package.json @@ -100,12 +100,12 @@ "electron-store": "8.2.0", "electron-updater": "6.3.9", "eslint": "8.57.1", - "eslint-config-prettier": "10.0.1", - "eslint-import-resolver-typescript": "3.7.0", + "eslint-config-prettier": "10.1.2", + "eslint-import-resolver-typescript": "3.10.1", "eslint-plugin-import": "2.31.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", - "eslint-plugin-storybook": "0.11.2", + "eslint-plugin-storybook": "0.12.0", "eslint-plugin-tailwindcss": "3.18.0", "html-loader": "5.1.0", "html-webpack-injector": "1.1.4", @@ -116,7 +116,7 @@ "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", "json5": "2.2.3", - "lint-staged": "15.4.1", + "lint-staged": "15.5.1", "mini-css-extract-plugin": "2.9.2", "nx": "20.8.0", "postcss": "8.5.1", @@ -136,7 +136,7 @@ "tsconfig-paths-webpack-plugin": "4.2.0", "type-fest": "2.19.0", "typescript": "5.4.2", - "typescript-eslint": "8.20.0", + "typescript-eslint": "8.30.1", "typescript-strict-plugin": "2.4.4", "url": "0.11.4", "util": "0.12.5", From c2c31e54c182bf160d36bb487356290ff51dbbf9 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:14:29 -0500 Subject: [PATCH 14/14] feat(auth): [PM-8978] migrate SSO connector to Tailwind - Convert Bootstrap styles to Tailwind - Remove deprecated sso.scss - Add test coverage for SSO connector [PM-8978] --- apps/web/src/connectors/sso.html | 22 +++-- apps/web/src/connectors/sso.scss | 1 - apps/web/src/connectors/sso.spec.ts | 119 ++++++++++++++++++++++++++++ apps/web/src/connectors/sso.ts | 8 +- apps/web/webpack.config.js | 2 +- 5 files changed, 132 insertions(+), 20 deletions(-) delete mode 100644 apps/web/src/connectors/sso.scss create mode 100644 apps/web/src/connectors/sso.spec.ts diff --git a/apps/web/src/connectors/sso.html b/apps/web/src/connectors/sso.html index 77996173cd7..55088c9f812 100644 --- a/apps/web/src/connectors/sso.html +++ b/apps/web/src/connectors/sso.html @@ -15,18 +15,16 @@ -
-
- -
-

- -

-
+
+ Bitwarden +
+

+ +

diff --git a/apps/web/src/connectors/sso.scss b/apps/web/src/connectors/sso.scss deleted file mode 100644 index a4c7f9b25b7..00000000000 --- a/apps/web/src/connectors/sso.scss +++ /dev/null @@ -1 +0,0 @@ -@import "../scss/styles.scss"; diff --git a/apps/web/src/connectors/sso.spec.ts b/apps/web/src/connectors/sso.spec.ts new file mode 100644 index 00000000000..45a41d94171 --- /dev/null +++ b/apps/web/src/connectors/sso.spec.ts @@ -0,0 +1,119 @@ +import { initiateWebAppSso, initiateBrowserSso } from "./sso"; + +describe("sso", () => { + let originalLocation: Location; + let originalPostMessage: any; + let postMessageSpy: jest.SpyInstance; + + beforeEach(() => { + // Save original window methods + originalLocation = window.location; + originalPostMessage = window.postMessage; + + // Mock location + Object.defineProperty(window, "location", { + value: { + href: "", + origin: "https://test.bitwarden.com", + }, + writable: true, + }); + + // Mock postMessage + postMessageSpy = jest.spyOn(window, "postMessage"); + + // Set up document + document.cookie = "ssoHandOffMessage=SSO login successful;SameSite=strict"; + const contentElement = document.createElement("div"); + contentElement.id = "content"; + document.body.appendChild(contentElement); + }); + + afterEach(() => { + // Restore original window methods + Object.defineProperty(window, "location", { value: originalLocation }); + window.postMessage = originalPostMessage; + + // Clean up document + const contentElement = document.getElementById("content"); + if (contentElement) { + document.body.removeChild(contentElement); + } + document.cookie = "ssoHandOffMessage=;SameSite=strict;max-age=0"; + + // Clear mocks + jest.clearAllMocks(); + }); + + describe("initiateWebAppSso", () => { + it("redirects to the SSO component with code and state", () => { + const code = "testcode"; + const state = "teststate"; + + initiateWebAppSso(code, state); + + expect(window.location.href).toBe( + "https://test.bitwarden.com/#/sso?code=testcode&state=teststate", + ); + }); + + it("redirects to the return URI when included in state", () => { + const code = "testcode"; + const state = "teststate_returnUri='/organizations'"; + + initiateWebAppSso(code, state); + + expect(window.location.href).toBe("https://test.bitwarden.com/#/organizations"); + }); + + it("handles empty code parameter", () => { + initiateWebAppSso("", "teststate"); + expect(window.location.href).toBe("https://test.bitwarden.com/#/sso?code=&state=teststate"); + }); + + it("handles empty state parameter", () => { + initiateWebAppSso("testcode", ""); + expect(window.location.href).toBe("https://test.bitwarden.com/#/sso?code=testcode&state="); + }); + }); + + describe("initiateBrowserSso", () => { + it("posts message with code and state", () => { + const code = "testcode"; + const state = "teststate"; + const lastpass = false; + + initiateBrowserSso(code, state, lastpass); + + expect(postMessageSpy).toHaveBeenCalledWith( + { command: "authResult", code, state, lastpass }, + window.location.origin, + ); + }); + + it("updates content with message from cookie", () => { + const code = "testcode"; + const state = "teststate"; + const lastpass = false; + + initiateBrowserSso(code, state, lastpass); + + const contentElement = document.getElementById("content"); + const paragraphElement = contentElement?.querySelector("p"); + expect(paragraphElement?.innerText).toBe("SSO login successful"); + }); + + it("handles lastpass flag correctly", () => { + const code = "testcode"; + const state = "teststate"; + const lastpass = true; + + initiateBrowserSso(code, state, lastpass); + + expect(postMessageSpy).toHaveBeenCalledWith( + { command: "authResult", code, state, lastpass }, + window.location.origin, + ); + }); + }); +}); diff --git a/apps/web/src/connectors/sso.ts b/apps/web/src/connectors/sso.ts index 886742c4c49..55d661b35e8 100644 --- a/apps/web/src/connectors/sso.ts +++ b/apps/web/src/connectors/sso.ts @@ -2,10 +2,6 @@ // @ts-strict-ignore import { getQsParam } from "./common"; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./sso.scss"); - window.addEventListener("load", () => { const code = getQsParam("code"); const state = getQsParam("state"); @@ -20,7 +16,7 @@ window.addEventListener("load", () => { } }); -function initiateWebAppSso(code: string, state: string) { +export function initiateWebAppSso(code: string, state: string) { // If we've initiated SSO from somewhere other than the SSO component on the web app, the SSO component will add // a _returnUri to the state variable. Here we're extracting that URI and sending the user there instead of to the SSO component. const returnUri = extractFromRegex(state, "(?<=_returnUri=')(.*)(?=')"); @@ -31,7 +27,7 @@ function initiateWebAppSso(code: string, state: string) { } } -function initiateBrowserSso(code: string, state: string, lastpass: boolean) { +export function initiateBrowserSso(code: string, state: string, lastpass: boolean) { window.postMessage({ command: "authResult", code, state, lastpass }, window.location.origin); const handOffMessage = ("; " + document.cookie) .split("; ssoHandOffMessage=") diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index 9ccccee21bf..a4ac3322200 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -122,7 +122,7 @@ const plugins = [ new HtmlWebpackPlugin({ template: "./src/connectors/sso.html", filename: "sso-connector.html", - chunks: ["connectors/sso"], + chunks: ["connectors/sso", "styles"], }), new HtmlWebpackPlugin({ template: "./src/connectors/redirect.html",