1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-15 16:05:03 +00:00

Expand generic pattern for notification queue messages. (#18543)

This commit is contained in:
blackwood
2026-02-12 10:39:41 -05:00
committed by GitHub
parent ad8bde057f
commit 7fcb1a7a76
3 changed files with 146 additions and 87 deletions

View File

@@ -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<T, D> {
// universal notification properties
export interface NotificationQueueMessage<T, D> {
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<LockedVaultPendingNotificationsData> &
@@ -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<void>;
bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise<FolderView[]>;
@@ -150,16 +150,3 @@ type NotificationBackgroundExtensionMessageHandlers = {
bgGetActiveUserServerConfig: () => Promise<ServerConfig | null>;
getWebVaultUrlForNotification: () => Promise<string>;
};
export {
AddChangePasswordNotificationQueueMessage,
AddLoginQueueMessage,
AddUnlockVaultQueueMessage,
NotificationQueueMessageItem,
LockedVaultPendingNotificationsData,
AdjustNotificationBarMessageData,
UnlockVaultMessageData,
AddLoginMessageData,
NotificationBackgroundExtensionMessage,
NotificationBackgroundExtensionMessageHandlers,
};

View File

@@ -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<CipherView>({
@@ -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<CipherView>({
@@ -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<CipherView>({

View File

@@ -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;