From d5b7af75e9b0083902654a7c5b06f41a53ad4b4f Mon Sep 17 00:00:00 2001 From: Jonathan Prusik Date: Fri, 11 Apr 2025 15:16:30 -0400 Subject: [PATCH 1/6] [PM-14909] Build components for security task completion notification (#14230) * squash split component work from pm-14909 * fix typing --- apps/browser/src/_locales/en/messages.json | 25 ++ .../components/buttons/action-button.ts | 9 +- .../content/components/icons/external-link.ts | 22 ++ .../content/components/icons/index.ts | 2 + .../content/components/icons/keyhole.ts | 222 ++++++++++++++++++ .../lit-stories/icons/icons.lit-stories.ts | 2 + .../notification/body.lit-stories.ts | 2 +- .../body.lit-stories.ts} | 26 +- .../confirmation/container.lit-stories.ts | 55 +++++ .../confirmation/footer.lit-stories.ts | 36 +++ .../confirmation/message.lit-stories.ts | 37 +++ .../notification/container.lit-stories.ts | 61 +++++ .../notification/footer.lit-stories.ts | 2 +- .../notification/header.lit-stories.ts | 2 +- .../notification/confirmation-message.ts | 53 ----- .../{confirmation.ts => confirmation/body.ts} | 48 ++-- .../container.ts} | 69 ++++-- .../notification/confirmation/footer.ts | 59 +++++ .../notification/confirmation/message.ts | 83 +++++++ .../components/notification/container.ts | 24 +- .../abstractions/notification-bar.ts | 6 + apps/browser/src/autofill/notification/bar.ts | 6 +- 22 files changed, 730 insertions(+), 121 deletions(-) create mode 100644 apps/browser/src/autofill/content/components/icons/external-link.ts create mode 100644 apps/browser/src/autofill/content/components/icons/keyhole.ts rename apps/browser/src/autofill/content/components/lit-stories/notification/{confirmation.lit-stories.ts => confirmation/body.lit-stories.ts} (53%) create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts create mode 100644 apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts delete mode 100644 apps/browser/src/autofill/content/components/notification/confirmation-message.ts rename apps/browser/src/autofill/content/components/notification/{confirmation.ts => confirmation/body.ts} (63%) rename apps/browser/src/autofill/content/components/notification/{confirmation-container.ts => confirmation/container.ts} (66%) create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation/footer.ts create mode 100644 apps/browser/src/autofill/content/components/notification/confirmation/message.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 586e7e1f2cf..f3b85496b75 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1098,6 +1098,31 @@ "message": "Login updated", "description": "Message displayed when login details are successfully updated." }, + "loginUpdateTaskSuccess": { + "message": "Great job! You took the steps to make you and $ORGANIZATION$ more secure.", + "placeholders": { + "organization": { + "content": "$1" + } + }, + "description": "Shown to user after login is updated." + }, + "loginUpdateTaskSuccessAdditional": { + "message": "Thank you for making $ORGANIZATION$ more secure. You have $TASK_COUNT$ more passwords to update.", + "placeholders": { + "organization": { + "content": "$1" + }, + "task_count": { + "content": "$2" + } + }, + "description": "Shown to user after login is updated." + }, + "nextSecurityTaskAction": { + "message": "Change next password", + "description": "Message prompting user to undertake completion of another security task." + }, "saveFailure": { "message": "Error saving", "description": "Error message shown when the system fails to save login details." diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts index f0642d4233a..881b44b4785 100644 --- a/apps/browser/src/autofill/content/components/buttons/action-button.ts +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -1,5 +1,5 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, TemplateResult } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; @@ -11,7 +11,7 @@ export function ActionButton({ theme, handleClick, }: { - buttonText: string; + buttonText: string | TemplateResult; disabled?: boolean; theme: Theme; handleClick: (e: Event) => void; @@ -63,4 +63,9 @@ const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: The color: ${themes[theme].text.contrast}; } `} + + svg { + width: fit-content; + height: 16px; + } `; diff --git a/apps/browser/src/autofill/content/components/icons/external-link.ts b/apps/browser/src/autofill/content/components/icons/external-link.ts new file mode 100644 index 00000000000..10c6d831025 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/external-link.ts @@ -0,0 +1,22 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { IconProps } from "../common-types"; +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function ExternalLink({ color, disabled, theme }: IconProps) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts index c4769a0e69d..4b6cb7abdd8 100644 --- a/apps/browser/src/autofill/content/components/icons/index.ts +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -4,9 +4,11 @@ export { BrandIconContainer } from "./brand-icon-container"; export { Business } from "./business"; export { Close } from "./close"; export { ExclamationTriangle } from "./exclamation-triangle"; +export { ExternalLink } from "./external-link"; export { Family } from "./family"; export { Folder } from "./folder"; export { Globe } from "./globe"; +export { Keyhole } from "./keyhole"; export { PartyHorn } from "./party-horn"; export { PencilSquare } from "./pencil-square"; export { Shield } from "./shield"; diff --git a/apps/browser/src/autofill/content/components/icons/keyhole.ts b/apps/browser/src/autofill/content/components/icons/keyhole.ts new file mode 100644 index 00000000000..0294c0c8499 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/keyhole.ts @@ -0,0 +1,222 @@ +import { html } from "lit"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { IconProps } from "../common-types"; + +// This icon has static multi-colors for each theme +export function Keyhole({ theme }: IconProps) { + if (theme === ThemeTypes.Dark) { + return html` + + + + + + + + + + + + + + + + + + + + + + `; + } + + return html` + + + + + + + + + + + + + + + + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts index 20c88a59246..8bd87ef6674 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/icons/icons.lit-stories.ts @@ -57,9 +57,11 @@ export const BusinessIcon = createIconStory("Business"); export const BrandIcon = createIconStory("BrandIconContainer"); export const CloseIcon = createIconStory("Close"); export const ExclamationTriangleIcon = createIconStory("ExclamationTriangle"); +export const ExternalLinkIcon = createIconStory("ExternalLink"); export const FamilyIcon = createIconStory("Family"); export const FolderIcon = createIconStory("Folder"); export const GlobeIcon = createIconStory("Globe"); +export const KeyholeIcon = createIconStory("Keyhole"); export const PartyHornIcon = createIconStory("PartyHorn"); export const PencilSquareIcon = createIconStory("PencilSquare"); export const ShieldIcon = createIconStory("Shield"); diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts index e43bc08b920..32b4170d1da 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts @@ -16,7 +16,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Body", + title: "Components/Notifications/Body", argTypes: { ciphers: { control: "object" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts similarity index 53% rename from apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts rename to apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts index b3dee95efd0..4d9be06fd7e 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/body.lit-stories.ts @@ -1,29 +1,26 @@ import { Meta, StoryObj } from "@storybook/web-components"; -import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; +import { ThemeTypes } from "@bitwarden/common/platform/enums"; -import { NotificationConfirmationBody } from "../../notification/confirmation"; - -type Args = { - buttonText: string; - confirmationMessage: string; - handleOpenVault: () => void; - theme: Theme; - error: string; -}; +import { + NotificationConfirmationBody, + NotificationConfirmationBodyProps, +} from "../../../notification/confirmation/body"; export default { - title: "Components/Notifications/Notification Confirmation Body", + title: "Components/Notifications/Confirmation/Body", argTypes: { error: { control: "text" }, buttonText: { control: "text" }, confirmationMessage: { control: "text" }, + messageDetails: { control: "text" }, theme: { control: "select", options: [...Object.values(ThemeTypes)] }, }, args: { error: "", buttonText: "View", confirmationMessage: "[item name] updated in Bitwarden.", + messageDetails: "You can view it in your vault.", theme: ThemeTypes.Light, }, parameters: { @@ -32,10 +29,11 @@ export default { url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", }, }, -} as Meta; +} as Meta; -const Template = (args: Args) => NotificationConfirmationBody({ ...args }); +const Template = (args: NotificationConfirmationBodyProps) => + NotificationConfirmationBody({ ...args }); -export const Default: StoryObj = { +export const Default: StoryObj = { render: Template, }; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts new file mode 100644 index 00000000000..ec7194004d8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/container.lit-stories.ts @@ -0,0 +1,55 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { NotificationTypes } from "../../../../../notification/abstractions/notification-bar"; +import { + NotificationConfirmationContainer, + NotificationConfirmationContainerProps, +} from "../../../notification/confirmation/container"; + +export default { + title: "Components/Notifications/Confirmation", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [NotificationTypes.Change, NotificationTypes.Add] }, + }, + args: { + error: "", + task: { + orgName: "Acme, Inc.", + remainingTasksCount: 0, + }, + handleCloseNotification: () => alert("Close notification action triggered"), + handleOpenTasks: () => alert("Open tasks action triggered"), + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + loginUpdateTaskSuccessAdditional: + "Thank you for making your organization more secure. You have 3 more passwords to update.", + loginUpdateTaskSuccess: + "Great job! You took the steps to make you and your organization more secure.", + nextSecurityTaskAction: "Change next password", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + view: "View", + }, + type: NotificationTypes.Change, + username: "Acme, Inc. Login", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationContainerProps) => + NotificationConfirmationContainer({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts new file mode 100644 index 00000000000..953fb90d067 --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/footer.lit-stories.ts @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum"; + +import { + NotificationConfirmationFooter, + NotificationConfirmationFooterProps, +} from "../../../notification/confirmation/footer"; + +export default { + title: "Components/Notifications/Confirmation/Footer", + argTypes: { + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + handleButtonClick: () => alert("Action button triggered"), + i18n: { + nextSecurityTaskAction: "Change next password", + }, + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=32-4949&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationFooterProps) => + html`
${NotificationConfirmationFooter({ ...args })}
`; + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts new file mode 100644 index 00000000000..f01503b331f --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/confirmation/message.lit-stories.ts @@ -0,0 +1,37 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { + NotificationConfirmationMessage, + NotificationConfirmationMessageProps, +} from "../../../notification/confirmation/message"; + +export default { + title: "Components/Notifications/Confirmation/Message", + argTypes: { + buttonText: { control: "text" }, + message: { control: "text" }, + messageDetails: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + }, + args: { + buttonText: "View", + message: "[item name] updated in Bitwarden.", + messageDetails: "It was added to your vault.", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationConfirmationMessageProps) => + NotificationConfirmationMessage({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts new file mode 100644 index 00000000000..351c971ec0e --- /dev/null +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/container.lit-stories.ts @@ -0,0 +1,61 @@ +import { Meta, StoryObj } from "@storybook/web-components"; + +import { ThemeTypes } from "@bitwarden/common/platform/enums"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { NotificationTypes } from "../../../../notification/abstractions/notification-bar"; +import { NotificationContainer, NotificationContainerProps } from "../../notification/container"; + +export default { + title: "Components/Notifications", + argTypes: { + error: { control: "text" }, + theme: { control: "select", options: [...Object.values(ThemeTypes)] }, + type: { control: "select", options: [...Object.values(NotificationTypes)] }, + }, + args: { + error: "", + ciphers: [ + { + id: "1", + name: "Example Cipher", + type: CipherType.Login, + favorite: false, + reprompt: CipherRepromptType.None, + icon: { + imageEnabled: true, + image: "", + fallbackImage: "https://example.com/fallback.png", + icon: "icon-class", + }, + login: { username: "user@example.com" }, + }, + ], + i18n: { + loginSaveSuccess: "Login saved", + loginUpdateSuccess: "Login updated", + saveAction: "Save", + saveAsNewLoginAction: "Save as new login", + saveFailure: "Error saving", + saveFailureDetails: "Oh no! We couldn't save this. Try entering the details manually.", + updateLoginPrompt: "Update existing login?", + view: "View", + }, + type: NotificationTypes.Change, + username: "mockUsername", + theme: ThemeTypes.Light, + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/design/LEhqLAcBPY8uDKRfU99n9W/Autofill-notification-redesign?node-id=485-20160&m=dev", + }, + }, +} as Meta; + +const Template = (args: NotificationContainerProps) => NotificationContainer({ ...args }); + +export const Default: StoryObj = { + render: Template, +}; diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts index ea2bbdc2e15..29d9955ec64 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/footer.lit-stories.ts @@ -7,7 +7,7 @@ import { NotificationFooter, NotificationFooterProps } from "../../notification/ import { mockFolderData, mockOrganizationData } from "../mock-data"; export default { - title: "Components/Notifications/Notification Footer", + title: "Components/Notifications/Footer", argTypes: { notificationType: { control: "select", diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts index 49cc1e6bd8d..0857c99130e 100644 --- a/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts +++ b/apps/browser/src/autofill/content/components/lit-stories/notification/header.lit-stories.ts @@ -12,7 +12,7 @@ type Args = { }; export default { - title: "Components/Notifications/Notification Header", + title: "Components/Notifications/Header", argTypes: { message: { control: "text" }, standalone: { control: "boolean" }, diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts b/apps/browser/src/autofill/content/components/notification/confirmation-message.ts deleted file mode 100644 index d6f7ba3024d..00000000000 --- a/apps/browser/src/autofill/content/components/notification/confirmation-message.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { css } from "@emotion/css"; -import { html } from "lit"; - -import { Theme } from "@bitwarden/common/platform/enums"; - -import { themes } from "../constants/styles"; - -export function NotificationConfirmationMessage({ - buttonText, - confirmationMessage, - handleClick, - theme, -}: { - buttonText: string; - confirmationMessage: string; - handleClick: (e: Event) => void; - theme: Theme; -}) { - return html` - ${confirmationMessage} - ${buttonText} - `; -} - -const baseTextStyles = css` - flex-grow: 1; - overflow-x: hidden; - text-align: left; - text-overflow: ellipsis; - line-height: 24px; - font-family: "DM Sans", sans-serif; - font-size: 16px; -`; - -const notificationConfirmationMessageStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].text.main}; - font-weight: 400; -`; - -const notificationConfirmationButtonTextStyles = (theme: Theme) => css` - ${baseTextStyles} - color: ${themes[theme].primary[600]}; - font-weight: 700; - cursor: pointer; -`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation.ts b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts similarity index 63% rename from apps/browser/src/autofill/content/components/notification/confirmation.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/body.ts index 8c213a7663f..55d257b36f4 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/body.ts @@ -1,12 +1,12 @@ import createEmotion from "@emotion/css/create-instance"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme } from "@bitwarden/common/platform/enums"; -import { themes } from "../constants/styles"; -import { PartyHorn, Warning } from "../icons"; +import { themes } from "../../constants/styles"; +import { PartyHorn, Keyhole, Warning } from "../../icons"; -import { NotificationConfirmationMessage } from "./confirmation-message"; +import { NotificationConfirmationMessage } from "./message"; export const componentClassPrefix = "notification-confirmation-body"; @@ -14,31 +14,41 @@ const { css } = createEmotion({ key: componentClassPrefix, }); -export function NotificationConfirmationBody({ - buttonText, - error, - confirmationMessage, - theme, - handleOpenVault, -}: { - error?: string; +export type NotificationConfirmationBodyProps = { buttonText: string; confirmationMessage: string; + error?: string; + messageDetails?: string; + tasksAreComplete?: boolean; theme: Theme; handleOpenVault: (e: Event) => void; -}) { - const IconComponent = !error ? PartyHorn : Warning; +}; + +export function NotificationConfirmationBody({ + buttonText, + confirmationMessage, + error, + messageDetails, + tasksAreComplete, + theme, + handleOpenVault, +}: NotificationConfirmationBodyProps) { + const IconComponent = tasksAreComplete ? Keyhole : !error ? PartyHorn : Warning; + + const showConfirmationMessage = confirmationMessage || buttonText || messageDetails; + return html`
${IconComponent({ theme })}
- ${confirmationMessage && buttonText + ${showConfirmationMessage ? NotificationConfirmationMessage({ - handleClick: handleOpenVault, - confirmationMessage, - theme, buttonText, + message: confirmationMessage, + messageDetails, + theme, + handleClick: handleOpenVault, }) - : null} + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts similarity index 66% rename from apps/browser/src/autofill/content/components/notification/confirmation-container.ts rename to apps/browser/src/autofill/content/components/notification/confirmation/container.ts index 0666859ac44..a071338af9a 100644 --- a/apps/browser/src/autofill/content/components/notification/confirmation-container.ts +++ b/apps/browser/src/autofill/content/components/notification/confirmation/container.ts @@ -1,42 +1,67 @@ import { css } from "@emotion/css"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { NotificationBarIframeInitData, - NotificationTypes, + NotificationTaskInfo, NotificationType, -} from "../../../notification/abstractions/notification-bar"; -import { themes, spacing } from "../constants/styles"; - -import { NotificationConfirmationBody } from "./confirmation"; + NotificationTypes, +} from "../../../../notification/abstractions/notification-bar"; +import { themes, spacing } from "../../constants/styles"; import { NotificationHeader, componentClassPrefix as notificationHeaderClassPrefix, -} from "./header"; +} from "../header"; + +import { NotificationConfirmationBody } from "./body"; +import { NotificationConfirmationFooter } from "./footer"; + +export type NotificationConfirmationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleOpenVault: (e: Event) => void; + handleOpenTasks: (e: Event) => void; +} & { + error?: string; + i18n: { [key: string]: string }; + task?: NotificationTaskInfo; + type: NotificationType; + username: string; +}; export function NotificationConfirmationContainer({ error, handleCloseNotification, handleOpenVault, + handleOpenTasks, i18n, + task, theme = ThemeTypes.Light, type, username, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleOpenVault: (e: Event) => void; -} & { - error?: string; - i18n: { [key: string]: string }; - type: NotificationType; - username: string; -}) { +}: NotificationConfirmationContainerProps) { const headerMessage = getHeaderMessage(i18n, type, error); const confirmationMessage = getConfirmationMessage(i18n, username, type, error); const buttonText = error ? i18n.newItem : i18n.view; + let messageDetails: string | undefined; + let remainingTasksCount: number | undefined; + let tasksAreComplete: boolean = false; + + if (task) { + remainingTasksCount = task.remainingTasksCount || 0; + tasksAreComplete = remainingTasksCount === 0; + + messageDetails = + remainingTasksCount > 0 + ? chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional", [ + task.orgName, + `${remainingTasksCount}`, + ]) + : chrome.i18n.getMessage("loginUpdateTaskSuccess", [task.orgName]); + } + return html`
${NotificationHeader({ @@ -47,10 +72,18 @@ export function NotificationConfirmationContainer({ ${NotificationConfirmationBody({ buttonText, confirmationMessage, - error: error, - handleOpenVault, + tasksAreComplete, + messageDetails, theme, + handleOpenVault, })} + ${remainingTasksCount + ? NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick: handleOpenTasks, + }) + : nothing}
`; } diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts new file mode 100644 index 00000000000..e245d22c8e8 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/footer.ts @@ -0,0 +1,59 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { ActionButton } from "../../buttons/action-button"; +import { spacing, themes } from "../../constants/styles"; +import { ExternalLink } from "../../icons"; + +export type NotificationConfirmationFooterProps = { + i18n: { [key: string]: string }; + theme: Theme; + handleButtonClick: (event: Event) => void; +}; + +export function NotificationConfirmationFooter({ + i18n, + theme, + handleButtonClick, +}: NotificationConfirmationFooterProps) { + const primaryButtonText = i18n.nextSecurityTaskAction; + + return html` +
+ ${ActionButton({ + handleClick: handleButtonClick, + buttonText: AdditionalTasksButtonContent({ buttonText: primaryButtonText, theme }), + theme, + })} +
+ `; +} + +const notificationConfirmationFooterStyles = ({ theme }: { theme: Theme }) => css` + background-color: ${themes[theme].background.alt}; + padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]}; + max-width: min-content; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + padding-bottom: ${spacing[4]}; + } +`; + +function AdditionalTasksButtonContent({ buttonText, theme }: { buttonText: string; theme: Theme }) { + return html` +
+ ${buttonText} + ${ExternalLink({ theme, color: themes[theme].text.contrast })} +
+ `; +} + +const additionalTasksButtonContentStyles = ({ theme }: { theme: Theme }) => css` + gap: ${spacing[2]}; + display: flex; + align-items: center; + white-space: nowrap; +`; diff --git a/apps/browser/src/autofill/content/components/notification/confirmation/message.ts b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts new file mode 100644 index 00000000000..c018371caff --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/confirmation/message.ts @@ -0,0 +1,83 @@ +import { css } from "@emotion/css"; +import { html, nothing } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography } from "../../constants/styles"; + +export type NotificationConfirmationMessageProps = { + buttonText?: string; + message?: string; + messageDetails?: string; + handleClick: (e: Event) => void; + theme: Theme; +}; + +export function NotificationConfirmationMessage({ + buttonText, + message, + messageDetails, + handleClick, + theme, +}: NotificationConfirmationMessageProps) { + return html` +
+ ${message || buttonText + ? html` + + ${message || nothing} + ${buttonText + ? html` + + ${buttonText} + + ` + : nothing} + + ` + : nothing} + ${messageDetails + ? html`
${messageDetails}
` + : nothing} +
+ `; +} + +const baseTextStyles = css` + flex-grow: 1; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + line-height: 24px; + font-family: "DM Sans", sans-serif; + font-size: 16px; +`; + +const notificationConfirmationMessageStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].text.main}; + font-weight: 400; +`; + +const notificationConfirmationButtonTextStyles = (theme: Theme) => css` + ${baseTextStyles} + + color: ${themes[theme].primary[600]}; + font-weight: 700; + cursor: pointer; +`; + +const AdditionalMessageStyles = ({ theme }: { theme: Theme }) => css` + ${typography.body2} + + font-size: 14px; + color: ${themes[theme].text.muted}; +`; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts index 8d80dc9fb50..c29f58e116b 100644 --- a/apps/browser/src/autofill/content/components/notification/container.ts +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -19,6 +19,18 @@ import { componentClassPrefix as notificationHeaderClassPrefix, } from "./header"; +export type NotificationContainerProps = NotificationBarIframeInitData & { + handleCloseNotification: (e: Event) => void; + handleSaveAction: (e: Event) => void; + handleEditOrUpdateAction: (e: Event) => void; +} & { + ciphers?: NotificationCipherData[]; + folders?: FolderView[]; + i18n: { [key: string]: string }; + organizations?: OrgView[]; + type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` +}; + export function NotificationContainer({ handleCloseNotification, handleEditOrUpdateAction, @@ -29,17 +41,7 @@ export function NotificationContainer({ organizations, theme = ThemeTypes.Light, type, -}: NotificationBarIframeInitData & { - handleCloseNotification: (e: Event) => void; - handleSaveAction: (e: Event) => void; - handleEditOrUpdateAction: (e: Event) => void; -} & { - ciphers?: NotificationCipherData[]; - folders?: FolderView[]; - i18n: { [key: string]: string }; - organizations?: OrgView[]; - type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` -}) { +}: NotificationContainerProps) { const headerMessage = getHeaderMessage(i18n, type); const showBody = true; diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index c138776ed0e..7e2fdab04d3 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -11,6 +11,11 @@ const NotificationTypes = { type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; +type NotificationTaskInfo = { + orgName: string; + remainingTasksCount: number; +}; + type NotificationBarIframeInitData = { ciphers?: NotificationCipherData[]; folders?: FolderView[]; @@ -38,6 +43,7 @@ type NotificationBarWindowMessageHandlers = { }; export { + NotificationTaskInfo, NotificationTypes, NotificationType, NotificationBarIframeInitData, diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 139b4551a24..f544e75527c 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -7,7 +7,7 @@ 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 { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container"; +import { NotificationConfirmationContainer } from "../content/components/notification/confirmation/container"; import { NotificationContainer } from "../content/components/notification/container"; import { buildSvgDomElement } from "../utils"; import { circleCheckIcon } from "../utils/svg-icons"; @@ -58,6 +58,9 @@ function getI18n() { loginSaveSuccessDetails: chrome.i18n.getMessage("loginSaveSuccessDetails"), loginUpdateSuccess: chrome.i18n.getMessage("loginUpdateSuccess"), loginUpdateSuccessDetails: chrome.i18n.getMessage("loginUpdatedSuccessDetails"), + loginUpdateTaskSuccess: chrome.i18n.getMessage("loginUpdateTaskSuccess"), + loginUpdateTaskSuccessAdditional: chrome.i18n.getMessage("loginUpdateTaskSuccessAdditional"), + nextSecurityTaskAction: chrome.i18n.getMessage("nextSecurityTaskAction"), newItem: chrome.i18n.getMessage("newItem"), never: chrome.i18n.getMessage("never"), notificationAddDesc: chrome.i18n.getMessage("notificationAddDesc"), @@ -369,6 +372,7 @@ function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) { error, username: username ?? i18n.typeLogin, handleOpenVault: (e) => cipherId && openViewVaultItemPopout(e, cipherId), + handleOpenTasks: () => {}, }), document.body, ); From 2fd83f830db77960367b2ddbf427bb9fed97e74f Mon Sep 17 00:00:00 2001 From: Jakub Gilis Date: Fri, 11 Apr 2025 21:30:06 +0200 Subject: [PATCH 2/6] Properly handle message aborts during cleanup (#13841) Replace the FallbackRequestedError rejection pattern with direct AbortController.abort() calls when destroying the Messenger. This eliminates misleading console errors and ensures correct cancellation behavior. The FallbackRequestedError is intended specifically for user-requested WebAuthn fallbacks, not general message cleanup operations. Fixes GitHub issue #12663 --- .../autofill/fido2/content/messaging/messenger.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apps/browser/src/autofill/fido2/content/messaging/messenger.ts b/apps/browser/src/autofill/fido2/content/messaging/messenger.ts index a5530d87a8e..ec7ff3bb7a4 100644 --- a/apps/browser/src/autofill/fido2/content/messaging/messenger.ts +++ b/apps/browser/src/autofill/fido2/content/messaging/messenger.ts @@ -1,7 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { FallbackRequestedError } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; - import { Message, MessageType } from "./message"; const SENDER = "bitwarden-webauthn"; @@ -126,17 +124,11 @@ export class Messenger { } }; - let onDestroyListener; - const destroyPromise: Promise = new Promise((_, reject) => { - onDestroyListener = () => reject(new FallbackRequestedError()); - this.onDestroy.addEventListener("destroy", onDestroyListener); - }); + const onDestroyListener = () => abortController.abort(); + this.onDestroy.addEventListener("destroy", onDestroyListener); try { - const handlerResponse = await Promise.race([ - this.handler(message, abortController), - destroyPromise, - ]); + const handlerResponse = await this.handler(message, abortController); port.postMessage({ ...handlerResponse, SENDER }); } catch (error) { port.postMessage({ From b90ede079d3671ea2c1a2a54cd133f01bf889c74 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 11 Apr 2025 22:55:02 -0400 Subject: [PATCH 3/6] [PM-18888] Fix duo redirect URL checks (#14174) * fix(PM-18888) : Create more strict checking of redirectURL to protect against open redirect attacks using regex. * fix : modify comments and check for embedded credentials. * feat : add testability to duo-redirect connector * fix : fixing strict typing; Removed styling from duo-redirect.ts which allows us to test without adding additional files and configurations for jest. * fix : remove duo-redirect.scss --- apps/web/src/connectors/duo-redirect.scss | 1 - apps/web/src/connectors/duo-redirect.spec.ts | 51 +++++++++++++ apps/web/src/connectors/duo-redirect.ts | 76 ++++++++++++++------ apps/web/webpack.config.js | 2 +- 4 files changed, 105 insertions(+), 25 deletions(-) delete mode 100644 apps/web/src/connectors/duo-redirect.scss create mode 100644 apps/web/src/connectors/duo-redirect.spec.ts diff --git a/apps/web/src/connectors/duo-redirect.scss b/apps/web/src/connectors/duo-redirect.scss deleted file mode 100644 index a4c7f9b25b7..00000000000 --- a/apps/web/src/connectors/duo-redirect.scss +++ /dev/null @@ -1 +0,0 @@ -@import "../scss/styles.scss"; diff --git a/apps/web/src/connectors/duo-redirect.spec.ts b/apps/web/src/connectors/duo-redirect.spec.ts new file mode 100644 index 00000000000..c0498861ba0 --- /dev/null +++ b/apps/web/src/connectors/duo-redirect.spec.ts @@ -0,0 +1,51 @@ +import { redirectToDuoFrameless } from "./duo-redirect"; + +describe("duo-redirect", () => { + describe("redirectToDuoFrameless", () => { + beforeEach(() => { + Object.defineProperty(window, "location", { + value: { href: "" }, + writable: true, + }); + }); + + it("should redirect to a valid Duo URL", () => { + const validUrl = "https://api-123.duosecurity.com/auth"; + redirectToDuoFrameless(validUrl); + expect(window.location.href).toBe(validUrl); + }); + + it("should redirect to a valid Duo Federal URL", () => { + const validUrl = "https://api-123.duofederal.com/auth"; + redirectToDuoFrameless(validUrl); + expect(window.location.href).toBe(validUrl); + }); + + it("should throw an error for an invalid URL", () => { + const invalidUrl = "https://malicious-site.com"; + expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for an malicious URL with valid redirect embedded", () => { + const invalidUrl = "https://malicious-site.com\\@api-123.duosecurity.com/auth"; + expect(() => redirectToDuoFrameless(invalidUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a non-HTTPS URL", () => { + const nonHttpsUrl = "http://api-123.duosecurity.com/auth"; + expect(() => redirectToDuoFrameless(nonHttpsUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a URL with an invalid hostname", () => { + const invalidHostnameUrl = "https://api-123.invalid.com"; + expect(() => redirectToDuoFrameless(invalidHostnameUrl)).toThrow("Invalid redirect URL"); + }); + + it("should throw an error for a URL with credentials", () => { + const UrlWithCredentials = "https://api-123.duosecurity.com:password@evil/attack"; + expect(() => redirectToDuoFrameless(UrlWithCredentials)).toThrow( + "Invalid redirect URL: embedded credentials not allowed", + ); + }); + }); +}); diff --git a/apps/web/src/connectors/duo-redirect.ts b/apps/web/src/connectors/duo-redirect.ts index c19e056d306..d1841247962 100644 --- a/apps/web/src/connectors/duo-redirect.ts +++ b/apps/web/src/connectors/duo-redirect.ts @@ -1,14 +1,8 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { getQsParam } from "./common"; import { TranslationService } from "./translation.service"; -// FIXME: Remove when updating file. Eslint update -// eslint-disable-next-line @typescript-eslint/no-require-imports -require("./duo-redirect.scss"); - const mobileDesktopCallback = "bitwarden://duo-callback"; -let localeService: TranslationService = null; +let localeService: TranslationService | null = null; window.addEventListener("load", async () => { const redirectUrl = getQsParam("duoFramelessUrl"); @@ -18,9 +12,18 @@ window.addEventListener("load", async () => { return; } - const client = getQsParam("client"); - const code = getQsParam("code"); - const state = getQsParam("state"); + const client: string | null = getQsParam("client"); + const code: string | null = getQsParam("code"); + const state: string | null = getQsParam("state"); + if (!client) { + throw new Error("client is null"); + } + if (!code) { + throw new Error("code is null"); + } + if (!state) { + throw new Error("state is null"); + } localeService = new TranslationService(navigator.language, "locales"); await localeService.init(); @@ -53,16 +56,28 @@ window.addEventListener("load", async () => { * validate the Duo AuthUrl and redirect to it. * @param redirectUrl the duo auth url */ -function redirectToDuoFrameless(redirectUrl: string) { - const validateUrl = new URL(redirectUrl); - const validDuoUrl = - validateUrl.protocol === "https:" && - (validateUrl.hostname.endsWith(".duosecurity.com") || - validateUrl.hostname.endsWith(".duofederal.com")); - - if (!validDuoUrl) { +export function redirectToDuoFrameless(redirectUrl: string) { + // Regex to match a valid duo redirect URL + /** + * This regex checks for the following: + * The string must start with "https://api-" + * Followed by a subdomain that can contain letters, numbers + * Followed by either "duosecurity.com" or "duofederal.com" + * This ensures that the redirect does not contain any malicious content + * and is a valid Duo URL. + * */ + const duoRedirectUrlRegex = /^https:\/\/api-[a-zA-Z0-9]+\.(duosecurity|duofederal)\.com/; + // Check if the redirect URL matches the regex + if (!duoRedirectUrlRegex.test(redirectUrl)) { throw new Error("Invalid redirect URL"); } + // At this point we know the URL to be valid, but we need to check for embedded credentials + const validateUrl = new URL(redirectUrl); + // URLs should not contain + // Check that no embedded credentials are present + if (validateUrl.username || validateUrl.password) { + throw new Error("Invalid redirect URL: embedded credentials not allowed"); + } window.location.href = decodeURIComponent(redirectUrl); } @@ -72,17 +87,23 @@ function redirectToDuoFrameless(redirectUrl: string) { * so browser, desktop, and mobile are not able to take advantage of the countdown timer or close button. */ function displayHandoffMessage(client: string) { - const content = document.getElementById("content"); + const content: HTMLElement | null = document.getElementById("content"); + if (!content) { + throw new Error("content element not found"); + } content.className = "text-center"; content.innerHTML = ""; const h1 = document.createElement("h1"); - const p = document.createElement("p"); + const p: HTMLElement = document.createElement("p"); + if (!localeService) { + throw new Error("localeService is not initialized"); + } h1.textContent = localeService.t("youSuccessfullyLoggedIn"); p.textContent = client == "web" - ? (p.textContent = localeService.t("thisWindowWillCloseIn5Seconds")) + ? localeService.t("thisWindowWillCloseIn5Seconds") : localeService.t("youMayCloseThisWindow"); h1.className = "font-weight-semibold"; @@ -102,11 +123,20 @@ function displayHandoffMessage(client: string) { }); content.appendChild(button); - // Countdown timer (closes tab upon completion) - let num = Number(p.textContent.match(/\d+/)[0]); + if (p.textContent === null) { + throw new Error("count down container is null"); + } + const counterString: string | null = p.textContent.match(/\d+/)?.[0] || null; + if (!counterString) { + throw new Error("count down time cannot be null"); + } + let num: number = Number(counterString); const interval = setInterval(() => { if (num > 1) { + if (p.textContent === null) { + throw new Error("count down container is null"); + } p.textContent = p.textContent.replace(String(num), String(num - 1)); num--; } else { diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index d172ea95c71..9ccccee21bf 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -142,7 +142,7 @@ const plugins = [ new HtmlWebpackPlugin({ template: "./src/connectors/duo-redirect.html", filename: "duo-redirect-connector.html", - chunks: ["connectors/duo-redirect"], + chunks: ["connectors/duo-redirect", "styles"], }), new HtmlWebpackPlugin({ template: "./src/404.html", From 8b64087b32d9e44105e2d037e600514de46627ef Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 14 Apr 2025 14:41:08 +0200 Subject: [PATCH 4/6] [PM-18040] Inject ipc content script dynamically (#13674) * feat: add content script manager * feat: inject into all pages * feat: only inject if flag is enabled * fix: wrong constructor parameters --- .../browser/src/background/main.background.ts | 3 ++ .../ipc/ipc-content-script-manager.service.ts | 42 +++++++++++++++++++ libs/common/src/enums/feature-flag.enum.ts | 6 +++ 3 files changed, 51 insertions(+) create mode 100644 apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 709d64f2094..a5001e0c5b7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -261,6 +261,7 @@ import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.s import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { IpcBackgroundService } from "../platform/ipc/ipc-background.service"; +import { IpcContentScriptManagerService } from "../platform/ipc/ipc-content-script-manager.service"; import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; @@ -405,6 +406,7 @@ export default class MainBackground { inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; taskService: TaskService; + ipcContentScriptManagerService: IpcContentScriptManagerService; ipcService: IpcService; onUpdatedRan: boolean; @@ -1314,6 +1316,7 @@ export default class MainBackground { this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + this.ipcContentScriptManagerService = new IpcContentScriptManagerService(this.configService); this.ipcService = new IpcBackgroundService(this.logService); } diff --git a/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts b/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts new file mode 100644 index 00000000000..e5fe95e2018 --- /dev/null +++ b/apps/browser/src/platform/ipc/ipc-content-script-manager.service.ts @@ -0,0 +1,42 @@ +import { mergeMap } from "rxjs"; + +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +import { BrowserApi } from "../browser/browser-api"; + +const IPC_CONTENT_SCRIPT_ID = "ipc-content-script"; + +export class IpcContentScriptManagerService { + constructor(configService: ConfigService) { + if (!BrowserApi.isManifestVersion(3)) { + // IPC not supported on MV2 + return; + } + + configService + .getFeatureFlag$(FeatureFlag.IpcChannelFramework) + .pipe( + mergeMap(async (enabled) => { + if (!enabled) { + return; + } + + try { + await BrowserApi.unregisterContentScriptsMv3({ ids: [IPC_CONTENT_SCRIPT_ID] }); + } catch { + // Ignore errors + } + + await BrowserApi.registerContentScriptsMv3([ + { + id: IPC_CONTENT_SCRIPT_ID, + matches: ["https://*/*"], + js: ["content/ipc-content-script.js"], + }, + ]); + }), + ) + .subscribe(); + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index cd88a415caf..09708859ac8 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -55,6 +55,9 @@ export enum FeatureFlag { VaultBulkManagementAction = "vault-bulk-management-action", SecurityTasks = "security-tasks", CipherKeyEncryption = "cipher-key-encryption", + + /* Platform */ + IpcChannelFramework = "ipc-channel-framework", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -118,6 +121,9 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PrivateKeyRegeneration]: FALSE, [FeatureFlag.UserKeyRotationV2]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, + + /* Platform */ + [FeatureFlag.IpcChannelFramework]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; From 5cc3ed7c5fba06c04b67e1870ea71322f21d72f5 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 14 Apr 2025 14:42:08 +0200 Subject: [PATCH 5/6] Move nodecryptofunctionservice codeownership (#14209) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6c7c43c4dac..2f402b15dd5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,8 +83,6 @@ libs/common/src/platform @bitwarden/team-platform-dev libs/common/spec @bitwarden/team-platform-dev libs/common/src/state-migrations @bitwarden/team-platform-dev libs/platform @bitwarden/team-platform-dev -# Node-specifc platform files -libs/node @bitwarden/team-key-management-dev # Web utils used across app and connectors apps/web/src/utils/ @bitwarden/team-platform-dev # Web core and shared files @@ -146,6 +144,8 @@ apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev libs/key-management-ui @bitwarden/team-key-management-dev libs/common/src/key-management @bitwarden/team-key-management-dev +# Node-cryptofunction service +libs/node @bitwarden/team-key-management-dev apps/desktop/desktop_native/core/src/biometric/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev From 8885f5da24c7ecebf98c947625bc8545dc840135 Mon Sep 17 00:00:00 2001 From: Alexander Aronov Date: Mon, 14 Apr 2025 14:42:41 +0200 Subject: [PATCH 6/6] [PM-19914][PM-19913] trim domains and long fields in forwarders (#14141) * PM-19913: Added max length to the generated_for and description peroperties in the FirefoxRelay API payload * [PM-19913] Added maxLength restriction to the website and generatedBy methods. Added maxLength limit of 200 to the description of addy.io --- .../integration/integration-context.spec.ts | 37 +++++++++++++++++++ .../tools/integration/integration-context.ts | 26 ++++++++++--- .../core/src/integration/addy-io.spec.ts | 4 ++ .../generator/core/src/integration/addy-io.ts | 2 +- .../src/integration/firefox-relay.spec.ts | 5 +++ .../core/src/integration/firefox-relay.ts | 4 +- 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/libs/common/src/tools/integration/integration-context.spec.ts b/libs/common/src/tools/integration/integration-context.spec.ts index 67a40afb337..33694aefea1 100644 --- a/libs/common/src/tools/integration/integration-context.spec.ts +++ b/libs/common/src/tools/integration/integration-context.spec.ts @@ -189,6 +189,33 @@ describe("IntegrationContext", () => { expect(result).toBe(""); }); + + it("extracts the hostname when extractHostname is true", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website( + { website: "https://www.example.com/path" }, + { extractHostname: true }, + ); + + expect(result).toBe("www.example.com"); + }); + + it("falls back to the full URL when Utils.getHost cannot extract the hostname", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website({ website: "invalid-url" }, { extractHostname: true }); + + expect(result).toBe("invalid-url"); + }); + + it("truncates the website to maxLength", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + + const result = context.website({ website: "www.example.com" }, { maxLength: 3 }); + + expect(result).toBe("www"); + }); }); describe("generatedBy", () => { @@ -211,5 +238,15 @@ describe("IntegrationContext", () => { expect(result).toBe("result"); expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedByWithWebsite", "www.example.com"); }); + + it("truncates generated text to maxLength", () => { + const context = new IntegrationContext(EXAMPLE_META, null, i18n); + i18n.t.mockReturnValue("This is the result text"); + + const result = context.generatedBy({ website: null }, { maxLength: 4 }); + + expect(result).toBe("This"); + expect(i18n.t).toHaveBeenCalledWith("forwarderGeneratedBy", ""); + }); }); }); diff --git a/libs/common/src/tools/integration/integration-context.ts b/libs/common/src/tools/integration/integration-context.ts index 40648df6803..49edafc026b 100644 --- a/libs/common/src/tools/integration/integration-context.ts +++ b/libs/common/src/tools/integration/integration-context.ts @@ -79,24 +79,40 @@ export class IntegrationContext { /** look up the website the integration is working with. * @param request supplies information about the state of the extension site + * @param options optional parameters + * @param options.extractHostname when `true`, tries to extract the hostname from the website URL, returns full URL otherwise + * @param options.maxLength limits the length of the return value * @returns The website or an empty string if a website isn't available * @remarks `website` is usually supplied when generating a credential from the vault */ - website(request: IntegrationRequest) { - return request.website ?? ""; + website( + request: IntegrationRequest, + options?: { extractHostname?: boolean; maxLength?: number }, + ) { + let url = request.website ?? ""; + if (options?.extractHostname) { + url = Utils.getHost(url) ?? url; + } + return url.slice(0, options?.maxLength); } /** look up localized text indicating Bitwarden requested the forwarding address. * @param request supplies information about the state of the extension site + * @param options optional parameters + * @param options.extractHostname when `true`, extracts the hostname from the website URL + * @param options.maxLength limits the length of the return value * @returns localized text describing a generated forwarding address */ - generatedBy(request: IntegrationRequest) { - const website = this.website(request); + generatedBy( + request: IntegrationRequest, + options?: { extractHostname?: boolean; maxLength?: number }, + ) { + const website = this.website(request, { extractHostname: options?.extractHostname ?? false }); const descriptionId = website === "" ? "forwarderGeneratedBy" : "forwarderGeneratedByWithWebsite"; const description = this.i18n.t(descriptionId, website); - return description; + return description.slice(0, options?.maxLength); } } diff --git a/libs/tools/generator/core/src/integration/addy-io.spec.ts b/libs/tools/generator/core/src/integration/addy-io.spec.ts index 9c816330616..40d17e9d888 100644 --- a/libs/tools/generator/core/src/integration/addy-io.spec.ts +++ b/libs/tools/generator/core/src/integration/addy-io.spec.ts @@ -55,6 +55,10 @@ describe("Addy.io forwarder", () => { const result = AddyIo.forwarder.createForwardingEmail.body(null, context); + expect(context.generatedBy).toHaveBeenCalledWith(null, { + extractHostname: true, + maxLength: 200, + }); expect(result).toEqual({ domain: "domain", description: "generated by", diff --git a/libs/tools/generator/core/src/integration/addy-io.ts b/libs/tools/generator/core/src/integration/addy-io.ts index 631c5fdb510..93ffed3392a 100644 --- a/libs/tools/generator/core/src/integration/addy-io.ts +++ b/libs/tools/generator/core/src/integration/addy-io.ts @@ -39,7 +39,7 @@ const createForwardingEmail = Object.freeze({ body(request: IntegrationRequest, context: ForwarderContext) { return { domain: context.emailDomain(), - description: context.generatedBy(request), + description: context.generatedBy(request, { extractHostname: true, maxLength: 200 }), }; }, hasJsonPayload(response: Response) { diff --git a/libs/tools/generator/core/src/integration/firefox-relay.spec.ts b/libs/tools/generator/core/src/integration/firefox-relay.spec.ts index ed487b7f49f..08798b154b3 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.spec.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.spec.ts @@ -56,6 +56,11 @@ describe("Firefox Relay forwarder", () => { const result = FirefoxRelay.forwarder.createForwardingEmail.body(null, context); + expect(context.website).toHaveBeenCalledWith(null, { maxLength: 255 }); + expect(context.generatedBy).toHaveBeenCalledWith(null, { + extractHostname: true, + maxLength: 64, + }); expect(result).toEqual({ enabled: true, generated_for: "website", diff --git a/libs/tools/generator/core/src/integration/firefox-relay.ts b/libs/tools/generator/core/src/integration/firefox-relay.ts index 9f40a3631ff..f80de0c95dd 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.ts @@ -33,8 +33,8 @@ const createForwardingEmail = Object.freeze({ body(request: IntegrationRequest, context: ForwarderContext) { return { enabled: true, - generated_for: context.website(request), - description: context.generatedBy(request), + generated_for: context.website(request, { maxLength: 255 }), + description: context.generatedBy(request, { extractHostname: true, maxLength: 64 }), }; }, hasJsonPayload(response: Response) {