From 7fcb1a7a7660ed2e1440b1a501c35a7dfc319389 Mon Sep 17 00:00:00 2001 From: blackwood Date: Thu, 12 Feb 2026 10:39:41 -0500 Subject: [PATCH] Expand generic pattern for notification queue messages. (#18543) --- .../abstractions/notification.background.ts | 99 ++++++++----------- .../notification.background.spec.ts | 96 ++++++++++++++---- .../background/notification.background.ts | 38 ++++--- 3 files changed, 146 insertions(+), 87 deletions(-) diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index e50a317e8a7..bc416d98634 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -4,70 +4,70 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CollectionView } from "../../content/components/common-types"; -import { NotificationType, NotificationTypes } from "../../enums/notification-type.enum"; +import { NotificationType } from "../../enums/notification-type.enum"; import AutofillPageDetails from "../../models/autofill-page-details"; /** - * @todo Remove Standard_ label when implemented as standard NotificationQueueMessage. + * Generic notification queue message structure. + * All notification types use this structure with type-specific data. */ -export interface Standard_NotificationQueueMessage { - // universal notification properties +export interface NotificationQueueMessage { domain: string; tab: chrome.tabs.Tab; launchTimestamp: number; expires: Date; wasVaultLocked: boolean; - - type: T; // NotificationType - data: D; // notification-specific data + type: T; + data: D; } -/** - * @todo Deprecate in favor of Standard_NotificationQueueMessage. - */ -interface NotificationQueueMessage { - type: NotificationTypes; - domain: string; - tab: chrome.tabs.Tab; - launchTimestamp: number; - expires: Date; - wasVaultLocked: boolean; -} +// Notification data type definitions +export type AddLoginNotificationData = { + username: string; + password: string; + uri: string; +}; -type ChangePasswordNotificationData = { +export type ChangePasswordNotificationData = { cipherIds: CipherView["id"][]; newPassword: string; }; -type AddChangePasswordNotificationQueueMessage = Standard_NotificationQueueMessage< +export type UnlockVaultNotificationData = never; + +export type AtRiskPasswordNotificationData = { + organizationName: string; + passwordChangeUri?: string; +}; + +// Notification queue message types using generic pattern +export type AddLoginQueueMessage = NotificationQueueMessage< + typeof NotificationType.AddLogin, + AddLoginNotificationData +>; + +export type AddChangePasswordNotificationQueueMessage = NotificationQueueMessage< typeof NotificationType.ChangePassword, ChangePasswordNotificationData >; -interface AddLoginQueueMessage extends NotificationQueueMessage { - type: "add"; - username: string; - password: string; - uri: string; -} +export type AddUnlockVaultQueueMessage = NotificationQueueMessage< + typeof NotificationType.UnlockVault, + UnlockVaultNotificationData +>; -interface AddUnlockVaultQueueMessage extends NotificationQueueMessage { - type: "unlock"; -} +export type AtRiskPasswordQueueMessage = NotificationQueueMessage< + typeof NotificationType.AtRiskPassword, + AtRiskPasswordNotificationData +>; -interface AtRiskPasswordQueueMessage extends NotificationQueueMessage { - type: "at-risk-password"; - organizationName: string; - passwordChangeUri?: string; -} - -type NotificationQueueMessageItem = +export type NotificationQueueMessageItem = | AddLoginQueueMessage | AddChangePasswordNotificationQueueMessage | AddUnlockVaultQueueMessage | AtRiskPasswordQueueMessage; -type LockedVaultPendingNotificationsData = { +export type LockedVaultPendingNotificationsData = { commandToRetry: { message: { command: string; @@ -80,26 +80,26 @@ type LockedVaultPendingNotificationsData = { target: string; }; -type AdjustNotificationBarMessageData = { +export type AdjustNotificationBarMessageData = { height: number; }; -type AddLoginMessageData = { +export type AddLoginMessageData = { username: string; password: string; url: string; }; -type UnlockVaultMessageData = { +export type UnlockVaultMessageData = { skipNotification?: boolean; }; /** - * @todo Extend generics to this type, see Standard_NotificationQueueMessage + * @todo Extend generics to this type, see NotificationQueueMessage * - use new `data` types as generic * - eliminate optional status of properties as needed per Notification Type */ -type NotificationBackgroundExtensionMessage = { +export type NotificationBackgroundExtensionMessage = { [key: string]: any; command: string; data?: Partial & @@ -119,7 +119,7 @@ type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage type BackgroundSenderParam = { sender: chrome.runtime.MessageSender }; type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; -type NotificationBackgroundExtensionMessageHandlers = { +export type NotificationBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; @@ -150,16 +150,3 @@ type NotificationBackgroundExtensionMessageHandlers = { bgGetActiveUserServerConfig: () => Promise; getWebVaultUrlForNotification: () => Promise; }; - -export { - AddChangePasswordNotificationQueueMessage, - AddLoginQueueMessage, - AddUnlockVaultQueueMessage, - NotificationQueueMessageItem, - LockedVaultPendingNotificationsData, - AdjustNotificationBarMessageData, - UnlockVaultMessageData, - AddLoginMessageData, - NotificationBackgroundExtensionMessage, - NotificationBackgroundExtensionMessageHandlers, -}; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 7d33d79a697..95d4111987b 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -126,9 +126,11 @@ describe("NotificationBackground", () => { it("returns a cipher view when passed an `AddLoginQueueMessage`", () => { const message: AddLoginQueueMessage = { type: "add", - username: "test", - password: "password", - uri: "https://example.com", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, domain: "", tab: createChromeTabMock(), expires: new Date(), @@ -140,13 +142,13 @@ describe("NotificationBackground", () => { expect(cipherView.name).toEqual("example.com"); expect(cipherView.login).toEqual({ fido2Credentials: [], - password: message.password, + password: message.data.password, uris: [ { - _uri: message.uri, + _uri: message.data.uri, }, ], - username: message.username, + username: message.data.username, }); }); @@ -154,9 +156,11 @@ describe("NotificationBackground", () => { const folderId = "folder-id"; const message: AddLoginQueueMessage = { type: "add", - username: "test", - password: "password", - uri: "https://example.com", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, domain: "example.com", tab: createChromeTabMock(), expires: new Date(), @@ -170,6 +174,44 @@ describe("NotificationBackground", () => { expect(cipherView.folderId).toEqual(folderId); }); + + it("removes 'www.' prefix from hostname when generating cipher name", () => { + const message: AddLoginQueueMessage = { + type: "add", + data: { + username: "test", + password: "password", + uri: "https://www.example.com", + }, + domain: "www.example.com", + tab: createChromeTabMock(), + expires: new Date(), + wasVaultLocked: false, + launchTimestamp: 0, + }; + const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message); + + expect(cipherView.name).toEqual("example.com"); + }); + + it("uses domain as fallback when hostname cannot be extracted from uri", () => { + const message: AddLoginQueueMessage = { + type: "add", + data: { + username: "test", + password: "password", + uri: "", + }, + domain: "fallback-domain.com", + tab: createChromeTabMock(), + expires: new Date(), + wasVaultLocked: false, + launchTimestamp: 0, + }; + const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message); + + expect(cipherView.name).toEqual("fallback-domain.com"); + }); }); describe("notification bar extension message handlers and triggers", () => { @@ -2544,8 +2586,11 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "updated-password", + data: { + username: "test", + password: "updated-password", + uri: "https://example.com", + }, wasVaultLocked: true, }); notificationBackground["notificationQueue"] = [queueMessage]; @@ -2559,7 +2604,7 @@ describe("NotificationBackground", () => { expect(updatePasswordSpy).toHaveBeenCalledWith( cipherView, - queueMessage.password, + queueMessage.data.password, message.edit, sender.tab, "testId", @@ -2631,9 +2676,14 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "password", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, wasVaultLocked: false, + launchTimestamp: Date.now(), + expires: new Date(Date.now() + 10000), }); notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ @@ -2670,9 +2720,14 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "password", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, wasVaultLocked: false, + launchTimestamp: Date.now(), + expires: new Date(Date.now() + 10000), }); notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ @@ -2716,9 +2771,14 @@ describe("NotificationBackground", () => { type: NotificationType.AddLogin, tab, domain: "example.com", - username: "test", - password: "password", + data: { + username: "test", + password: "password", + uri: "https://example.com", + }, wasVaultLocked: false, + launchTimestamp: Date.now(), + expires: new Date(Date.now() + 10000), }); notificationBackground["notificationQueue"] = [queueMessage]; const cipherView = mock({ diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index e97672c1f0d..3713cd7c4c2 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -68,6 +68,7 @@ import { AddChangePasswordNotificationQueueMessage, AddLoginQueueMessage, AddLoginMessageData, + AtRiskPasswordQueueMessage, NotificationQueueMessageItem, LockedVaultPendingNotificationsData, NotificationBackgroundExtensionMessage, @@ -528,12 +529,14 @@ export default class NotificationBackground { this.removeTabFromNotificationQueue(tab); const launchTimestamp = new Date().getTime(); - const queueMessage: NotificationQueueMessageItem = { + const queueMessage: AtRiskPasswordQueueMessage = { domain, wasVaultLocked, type: NotificationType.AtRiskPassword, - passwordChangeUri, - organizationName: organization.name, + data: { + passwordChangeUri, + organizationName: organization.name, + }, tab: tab, launchTimestamp, expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), @@ -612,10 +615,12 @@ export default class NotificationBackground { const launchTimestamp = new Date().getTime(); const message: AddLoginQueueMessage = { type: NotificationType.AddLogin, - username: loginInfo.username, - password: loginInfo.password, + data: { + username: loginInfo.username, + password: loginInfo.password, + uri: loginInfo.url, + }, domain: loginDomain, - uri: loginInfo.url, tab: tab, launchTimestamp, expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), @@ -1291,16 +1296,23 @@ export default class NotificationBackground { // If the vault was locked, check if a cipher needs updating instead of creating a new one if (queueMessage.wasVaultLocked) { const allCiphers = await this.cipherService.getAllDecryptedForUrl( - queueMessage.uri, + queueMessage.data.uri, activeUserId, ); const existingCipher = allCiphers.find( (c) => - c.login.username != null && c.login.username.toLowerCase() === queueMessage.username, + c.login.username != null && + c.login.username.toLowerCase() === queueMessage.data.username, ); if (existingCipher != null) { - await this.updatePassword(existingCipher, queueMessage.password, edit, tab, activeUserId); + await this.updatePassword( + existingCipher, + queueMessage.data.password, + edit, + tab, + activeUserId, + ); return; } } @@ -1721,15 +1733,15 @@ export default class NotificationBackground { folderId?: string, ): CipherView { const uriView = new LoginUriView(); - uriView.uri = message.uri; + uriView.uri = message.data.uri; const loginView = new LoginView(); loginView.uris = [uriView]; - loginView.username = message.username; - loginView.password = message.password; + loginView.username = message.data.username; + loginView.password = message.data.password; const cipherView = new CipherView(); - cipherView.name = (Utils.getHostname(message.uri) || message.domain).replace(/^www\./, ""); + cipherView.name = (Utils.getHostname(message.data.uri) || message.domain).replace(/^www\./, ""); cipherView.folderId = folderId; cipherView.type = CipherType.Login; cipherView.login = loginView;