From 34cd41988a5612d041fe56d4c239577a60b3f7ec Mon Sep 17 00:00:00 2001 From: neuronull <9162534+neuronull@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:44:08 -0600 Subject: [PATCH 01/19] Remove `EnableNewCardCombinedExpiryAutofill` feature flag (#16131) --- .../services/autofill.service.spec.ts | 48 +----- .../src/autofill/services/autofill.service.ts | 157 +----------------- libs/common/src/enums/feature-flag.enum.ts | 2 - 3 files changed, 2 insertions(+), 205 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 80cce5228d3..f9430387c83 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -14,7 +14,7 @@ import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/au import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag, FeatureFlagValueType } from "@bitwarden/common/enums/feature-flag.enum"; +import { FeatureFlagValueType } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -2987,12 +2987,6 @@ describe("AutofillService", () => { options.cipher.card.expMonth = "5"; } - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(false); - const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, @@ -3003,23 +2997,6 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", dateFormat[1]]); }); }); - - it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", async () => { - const value = await autofillService["generateCardFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(false); - - expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "2024-05"]); - }); }); const extraExpectedDateFormats = [ @@ -3092,12 +3069,6 @@ describe("AutofillService", () => { options.cipher.card.expMonth = "05"; } - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(true); - const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, @@ -3108,23 +3079,6 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", dateFormat[1]]); }); }); - - it("feature-flagged logic returns an expiration date format matching `mm/yy` if no valid format can be identified", async () => { - const value = await autofillService["generateCardFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - expect(enableNewCardCombinedExpiryAutofill).toEqual(true); - - expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "05/24"]); - }); }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index fd707ef96b3..51c0dd3f247 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -29,7 +29,6 @@ import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategySetting, UriMatchStrategy, @@ -1212,161 +1211,7 @@ export default class AutofillService implements AutofillServiceInterface { AutofillService.hasValue(card.expMonth) && AutofillService.hasValue(card.expYear) ) { - let combinedExpiryFillValue = null; - - const enableNewCardCombinedExpiryAutofill = await this.configService.getFeatureFlag( - FeatureFlag.EnableNewCardCombinedExpiryAutofill, - ); - - if (enableNewCardCombinedExpiryAutofill) { - combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp); - } else { - const fullMonth = ("0" + card.expMonth).slice(-2); - - let fullYear: string = card.expYear; - let partYear: string = null; - if (fullYear.length === 2) { - partYear = fullYear; - fullYear = normalizeExpiryYearFormat(fullYear); - } else if (fullYear.length === 4) { - partYear = fullYear.substr(2, 2); - } - - for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) { - if ( - // mm/yyyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "/" + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - combinedExpiryFillValue = fullMonth + "/" + fullYear; - } else if ( - // mm/yy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "/" + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = fullMonth + "/" + partYear; - } else if ( - // yyyy/mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - "/" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - combinedExpiryFillValue = fullYear + "/" + fullMonth; - } else if ( - // yy/mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - "/" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = partYear + "/" + fullMonth; - } else if ( - // mm-yyyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "-" + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - combinedExpiryFillValue = fullMonth + "-" + fullYear; - } else if ( - // mm-yy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "-" + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = fullMonth + "-" + partYear; - } else if ( - // yyyy-mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - "-" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - combinedExpiryFillValue = fullYear + "-" + fullMonth; - } else if ( - // yy-mm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - "-" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = partYear + "-" + fullMonth; - } else if ( - // yyyymm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - combinedExpiryFillValue = fullYear + fullMonth; - } else if ( - // yymm - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = partYear + fullMonth; - } else if ( - // mmyyyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - combinedExpiryFillValue = fullMonth + fullYear; - } else if ( - // mmyy - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - combinedExpiryFillValue = fullMonth + partYear; - } - - if (combinedExpiryFillValue != null) { - break; - } - } - - // If none of the previous cases applied, set as default - if (combinedExpiryFillValue == null) { - combinedExpiryFillValue = fullYear + "-" + fullMonth; - } - } + const combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp); this.makeScriptActionWithValue( fillScript, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 67f68e12847..cd9bbfc54c3 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -17,7 +17,6 @@ export enum FeatureFlag { PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals", /* Autofill */ - EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", NotificationRefresh = "notification-refresh", UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", MacOsNativeCredentialSync = "macos-native-credential-sync", @@ -74,7 +73,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CreateDefaultLocation]: FALSE, /* Autofill */ - [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, [FeatureFlag.NotificationRefresh]: FALSE, [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, From c72fdebfd934d823ed19b4313afa13a92ac991c2 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Tue, 26 Aug 2025 22:55:29 +0000 Subject: [PATCH 02/19] Bumped Desktop client to 2025.8.2 --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 42eb7017e03..ff958c0d8e6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2025.8.1", + "version": "2025.8.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 6daff35e115..4be839e28c2 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2025.8.1", + "version": "2025.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2025.8.1", + "version": "2025.8.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index ea2e8affda2..77b1f186171 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2025.8.1", + "version": "2025.8.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index c414adb416d..842480f56da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -277,7 +277,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.8.1", + "version": "2025.8.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -403,6 +403,7 @@ "license": "GPL-3.0" }, "libs/state-internal": { + "name": "@bitwarden/state-internal", "version": "0.0.1", "license": "GPL-3.0" }, From 4f09ae52abb8a83bc26c33e584f48b7215d72225 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:10:32 +1000 Subject: [PATCH 03/19] [PM-25203] Resolve circular dependencies through LooseComponentsModule (#16157) * Update modules to not import loose-components Instead they should import their dependencies directly. Only OssModule imports loose-components.module.ts. * Remove unused imports and exports --- .../organizations/collections/vault.module.ts | 2 - .../organizations/members/members.module.ts | 4 +- .../organizations/organization.module.ts | 4 +- .../organizations/policies/policies.module.ts | 5 ++- .../organization-reporting.module.ts | 9 +--- .../settings/organization-settings.module.ts | 9 +++- .../two-factor/two-factor-setup.component.ts | 5 ++- .../organization-billing.module.ts | 4 +- apps/web/src/app/oss.module.ts | 7 ++- apps/web/src/app/shared/index.ts | 1 - .../src/app/shared/loose-components.module.ts | 44 +------------------ .../app/tools/import/org-import.component.ts | 5 ++- .../org-vault-export.component.ts | 5 ++- .../vault/individual-vault/vault.module.ts | 3 +- .../device-approvals.component.ts | 4 +- .../organizations/organizations.module.ts | 4 +- 16 files changed, 38 insertions(+), 77 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts index 037a27cd781..1a093ff8352 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.module.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; -import { LooseComponentsModule } from "../../../shared/loose-components.module"; import { SharedModule } from "../../../shared/shared.module"; import { OrganizationBadgeModule } from "../../../vault/individual-vault/organization-badge/organization-badge.module"; import { ViewComponent } from "../../../vault/individual-vault/view.component"; @@ -15,7 +14,6 @@ import { VaultComponent } from "./vault.component"; imports: [ VaultRoutingModule, SharedModule, - LooseComponentsModule, GroupBadgeModule, CollectionNameBadgeComponent, OrganizationBadgeModule, diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index efc091cb335..e5bc5f29a3b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -6,7 +6,7 @@ import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; import { ScrollLayoutDirective } from "@bitwarden/components"; import { OrganizationFreeTrialWarningComponent } from "@bitwarden/web-vault/app/billing/organizations/warnings/components"; -import { LooseComponentsModule } from "../../../shared"; +import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedOrganizationModule } from "../shared"; import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component"; @@ -22,10 +22,10 @@ import { MembersComponent } from "./members.component"; @NgModule({ imports: [ SharedOrganizationModule, - LooseComponentsModule, MembersRoutingModule, UserDialogModule, PasswordCalloutComponent, + HeaderModule, ScrollingModule, PasswordStrengthV2Component, ScrollLayoutDirective, diff --git a/apps/web/src/app/admin-console/organizations/organization.module.ts b/apps/web/src/app/admin-console/organizations/organization.module.ts index d956174149b..2f0077d313e 100644 --- a/apps/web/src/app/admin-console/organizations/organization.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization.module.ts @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; import { ScrollLayoutDirective } from "@bitwarden/components"; import { OrganizationWarningsModule } from "@bitwarden/web-vault/app/billing/organizations/warnings/organization-warnings.module"; -import { LooseComponentsModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; import { CoreOrganizationModule } from "./core"; import { GroupAddEditComponent } from "./manage/group-add-edit.component"; @@ -19,7 +19,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; AccessSelectorModule, CoreOrganizationModule, OrganizationsRoutingModule, - LooseComponentsModule, + HeaderModule, ScrollingModule, ScrollLayoutDirective, OrganizationWarningsModule, diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.module.ts b/apps/web/src/app/admin-console/organizations/policies/policies.module.ts index 3999f36ecad..95b22497eba 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.module.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.module.ts @@ -1,6 +1,7 @@ import { NgModule } from "@angular/core"; -import { LooseComponentsModule, SharedModule } from "../../../shared"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; import { DisableSendPolicyComponent } from "./disable-send.component"; import { MasterPasswordPolicyComponent } from "./master-password.component"; @@ -17,7 +18,7 @@ import { SingleOrgPolicyComponent } from "./single-org.component"; import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authentication.component"; @NgModule({ - imports: [SharedModule, LooseComponentsModule], + imports: [SharedModule, HeaderModule], declarations: [ DisableSendPolicyComponent, MasterPasswordPolicyComponent, diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts index 22c3edcf240..46599d7da46 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting.module.ts @@ -1,19 +1,14 @@ import { NgModule } from "@angular/core"; import { ReportsSharedModule } from "../../../dirt/reports"; -import { LooseComponentsModule } from "../../../shared"; +import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared/shared.module"; import { OrganizationReportingRoutingModule } from "./organization-reporting-routing.module"; import { ReportsHomeComponent } from "./reports-home.component"; @NgModule({ - imports: [ - SharedModule, - ReportsSharedModule, - OrganizationReportingRoutingModule, - LooseComponentsModule, - ], + imports: [SharedModule, ReportsSharedModule, OrganizationReportingRoutingModule, HeaderModule], declarations: [ReportsHomeComponent], }) export class OrganizationReportingModule {} diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts index bfff3b2aa2e..9b0c3035e98 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings.module.ts @@ -2,8 +2,11 @@ import { NgModule } from "@angular/core"; import { ItemModule } from "@bitwarden/components"; -import { LooseComponentsModule, SharedModule } from "../../../shared"; +import { DangerZoneComponent } from "../../../auth/settings/account/danger-zone.component"; +import { HeaderModule } from "../../../layouts/header/header.module"; +import { SharedModule } from "../../../shared"; import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component"; +import { PremiumBadgeComponent } from "../../../vault/components/premium-badge.component"; import { PoliciesModule } from "../../organizations/policies"; import { AccountComponent } from "./account.component"; @@ -13,10 +16,12 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component"; @NgModule({ imports: [ SharedModule, - LooseComponentsModule, PoliciesModule, OrganizationSettingsRoutingModule, AccountFingerprintComponent, + DangerZoneComponent, + HeaderModule, + PremiumBadgeComponent, ItemModule, ], declarations: [AccountComponent, TwoFactorSetupComponent], diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 7259c3f0fe8..e538e72a77e 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -33,8 +33,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DialogRef, DialogService, ItemModule } from "@bitwarden/components"; -import { LooseComponentsModule } from "../../../shared/loose-components.module"; +import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared/shared.module"; +import { PremiumBadgeComponent } from "../../../vault/components/premium-badge.component"; import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component"; import { TwoFactorSetupAuthenticatorComponent } from "./two-factor-setup-authenticator.component"; @@ -47,7 +48,7 @@ import { TwoFactorVerifyComponent } from "./two-factor-verify.component"; @Component({ selector: "app-two-factor-setup", templateUrl: "two-factor-setup.component.html", - imports: [ItemModule, LooseComponentsModule, SharedModule], + imports: [ItemModule, HeaderModule, PremiumBadgeComponent, SharedModule], }) export class TwoFactorSetupComponent implements OnInit, OnDestroy { organizationId: string; diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index d8f4b7393aa..707a854de02 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -4,7 +4,7 @@ import { NgModule } from "@angular/core"; // eslint-disable-next-line no-restricted-imports import { BannerModule } from "../../../../../../libs/components/src/banner/banner.module"; import { UserVerificationModule } from "../../auth/shared/components/user-verification"; -import { LooseComponentsModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; import { BillingSharedModule } from "../shared"; import { AdjustSubscription } from "./adjust-subscription.component"; @@ -29,7 +29,7 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; UserVerificationModule, BillingSharedModule, OrganizationPlansComponent, - LooseComponentsModule, + HeaderModule, BannerModule, ], declarations: [ diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index d5fe718412a..4e04910246f 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -3,7 +3,9 @@ import { NgModule } from "@angular/core"; import { AuthModule } from "./auth"; import { LoginModule } from "./auth/login/login.module"; import { TrialInitiationModule } from "./billing/trial-initiation/trial-initiation.module"; -import { LooseComponentsModule, SharedModule } from "./shared"; +import { HeaderModule } from "./layouts/header/header.module"; +import { SharedModule } from "./shared"; +import { LooseComponentsModule } from "./shared/loose-components.module"; import { AccessComponent } from "./tools/send/send-access/access.component"; import { OrganizationBadgeModule } from "./vault/individual-vault/organization-badge/organization-badge.module"; import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-filter.module"; @@ -15,6 +17,7 @@ import "./shared/locales"; imports: [ SharedModule, LooseComponentsModule, + HeaderModule, TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, @@ -24,7 +27,7 @@ import "./shared/locales"; ], exports: [ SharedModule, - LooseComponentsModule, + HeaderModule, TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, diff --git a/apps/web/src/app/shared/index.ts b/apps/web/src/app/shared/index.ts index 7defcdedfda..7a1160c4105 100644 --- a/apps/web/src/app/shared/index.ts +++ b/apps/web/src/app/shared/index.ts @@ -1,2 +1 @@ export * from "./shared.module"; -export * from "./loose-components.module"; diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index d7dbdbc4ae5..f7f3aa3bfee 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -1,18 +1,7 @@ import { NgModule } from "@angular/core"; -import { - PasswordCalloutComponent, - UserVerificationFormInputComponent, - VaultTimeoutInputComponent, -} from "@bitwarden/auth/angular"; -import { LayoutComponent, NavigationModule } from "@bitwarden/components"; - -import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; -import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/manage/verify-recover-delete-org.component"; import { RecoverDeleteComponent } from "../auth/recover-delete.component"; import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component"; -import { DangerZoneComponent } from "../auth/settings/account/danger-zone.component"; -import { UserVerificationModule } from "../auth/shared/components/user-verification"; import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "../auth/verify-recover-delete.component"; import { FreeBitwardenFamiliesComponent } from "../billing/members/free-bitwarden-families.component"; @@ -30,33 +19,15 @@ import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../dirt/reports/pages/organizations/weak-passwords-report.component"; import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component"; import { HeaderModule } from "../layouts/header/header.module"; -import { PremiumBadgeComponent } from "../vault/components/premium-badge.component"; import { OrganizationBadgeModule } from "../vault/individual-vault/organization-badge/organization-badge.module"; import { PipesModule } from "../vault/individual-vault/pipes/pipes.module"; -import { AccountFingerprintComponent } from "./components/account-fingerprint/account-fingerprint.component"; import { SharedModule } from "./shared.module"; // Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left. // If you are building new functionality, please create or extend a feature module instead. @NgModule({ - imports: [ - SharedModule, - UserVerificationModule, - AccountFingerprintComponent, - OrganizationBadgeModule, - PipesModule, - PasswordCalloutComponent, - UserVerificationFormInputComponent, - DangerZoneComponent, - LayoutComponent, - NavigationModule, - HeaderModule, - OrganizationLayoutComponent, - VerifyRecoverDeleteOrgComponent, - VaultTimeoutInputComponent, - PremiumBadgeComponent, - ], + imports: [SharedModule, HeaderModule, OrganizationBadgeModule, PipesModule], declarations: [ OrgExposedPasswordsReportComponent, OrgInactiveTwoFactorReportComponent, @@ -73,25 +44,12 @@ import { SharedModule } from "./shared.module"; VerifyRecoverDeleteComponent, ], exports: [ - UserVerificationModule, - PremiumBadgeComponent, - OrganizationLayoutComponent, - OrgExposedPasswordsReportComponent, - OrgInactiveTwoFactorReportComponent, - OrgReusedPasswordsReportComponent, - OrgUnsecuredWebsitesReportComponent, - OrgWeakPasswordsReportComponent, - PremiumBadgeComponent, RecoverDeleteComponent, RecoverTwoFactorComponent, RemovePasswordComponent, SponsoredFamiliesComponent, - FreeBitwardenFamiliesComponent, - SponsoringOrgRowComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, - HeaderModule, - DangerZoneComponent, ], }) export class LooseComponentsModule {} diff --git a/apps/web/src/app/tools/import/org-import.component.ts b/apps/web/src/app/tools/import/org-import.component.ts index fd833f3a698..8fb5a582b1a 100644 --- a/apps/web/src/app/tools/import/org-import.component.ts +++ b/apps/web/src/app/tools/import/org-import.component.ts @@ -14,13 +14,14 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ImportCollectionServiceAbstraction } from "@bitwarden/importer-core"; import { ImportComponent } from "@bitwarden/importer-ui"; -import { LooseComponentsModule, SharedModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; import { ImportCollectionAdminService } from "./import-collection-admin.service"; @Component({ templateUrl: "org-import.component.html", - imports: [SharedModule, ImportComponent, LooseComponentsModule], + imports: [SharedModule, ImportComponent, HeaderModule], providers: [ { provide: ImportCollectionServiceAbstraction, diff --git a/apps/web/src/app/tools/vault-export/org-vault-export.component.ts b/apps/web/src/app/tools/vault-export/org-vault-export.component.ts index 94cc9bf18f7..a1de4814a13 100644 --- a/apps/web/src/app/tools/vault-export/org-vault-export.component.ts +++ b/apps/web/src/app/tools/vault-export/org-vault-export.component.ts @@ -5,11 +5,12 @@ import { ActivatedRoute } from "@angular/router"; import { ExportComponent } from "@bitwarden/vault-export-ui"; -import { LooseComponentsModule, SharedModule } from "../../shared"; +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; @Component({ templateUrl: "org-vault-export.component.html", - imports: [SharedModule, ExportComponent, LooseComponentsModule], + imports: [SharedModule, ExportComponent, HeaderModule], }) export class OrganizationVaultExportComponent implements OnInit { protected routeOrgId: string = null; diff --git a/apps/web/src/app/vault/individual-vault/vault.module.ts b/apps/web/src/app/vault/individual-vault/vault.module.ts index 57d3df30df7..573eceef64a 100644 --- a/apps/web/src/app/vault/individual-vault/vault.module.ts +++ b/apps/web/src/app/vault/individual-vault/vault.module.ts @@ -3,7 +3,7 @@ import { NgModule } from "@angular/core"; import { CollectionNameBadgeComponent } from "../../admin-console/organizations/collections"; import { GroupBadgeModule } from "../../admin-console/organizations/collections/group-badge/group-badge.module"; import { CollectionDialogComponent } from "../../admin-console/organizations/shared/components/collection-dialog"; -import { LooseComponentsModule, SharedModule } from "../../shared"; +import { SharedModule } from "../../shared"; import { BulkDialogsModule } from "./bulk-action-dialogs/bulk-dialogs.module"; import { OrganizationBadgeModule } from "./organization-badge/organization-badge.module"; @@ -20,7 +20,6 @@ import { ViewComponent } from "./view.component"; CollectionNameBadgeComponent, PipesModule, SharedModule, - LooseComponentsModule, BulkDialogsModule, CollectionDialogComponent, VaultComponent, diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts index f41b91261f7..bd62d972500 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/manage/device-approvals/device-approvals.component.ts @@ -21,7 +21,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { TableDataSource, NoItemsModule, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; -import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; @Component({ @@ -43,7 +43,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; ], }), ] satisfies SafeProvider[], - imports: [SharedModule, NoItemsModule, LooseComponentsModule], + imports: [SharedModule, NoItemsModule, HeaderModule], }) export class DeviceApprovalsComponent implements OnInit, OnDestroy { tableDataSource = new TableDataSource(); diff --git a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts index e19d028b007..9fce712e325 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/organizations/organizations.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { LooseComponentsModule } from "@bitwarden/web-vault/app/shared"; +import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; import { SsoComponent } from "../../auth/sso/sso.component"; @@ -11,7 +11,7 @@ import { ScimComponent } from "./manage/scim.component"; import { OrganizationsRoutingModule } from "./organizations-routing.module"; @NgModule({ - imports: [SharedModule, OrganizationsRoutingModule, LooseComponentsModule], + imports: [SharedModule, OrganizationsRoutingModule, HeaderModule], declarations: [ SsoComponent, ScimComponent, From 7ccb94d0a22dadde6c3d83b361301d0d067c2c1c Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 27 Aug 2025 09:25:36 -0400 Subject: [PATCH 04/19] fix missing null check (#16180) --- .../components/collection-dialog/collection-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts index 52c418a5211..5b207b5a688 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/collection-dialog/collection-dialog.component.ts @@ -421,7 +421,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { collectionView.users = this.formGroup.controls.access.value .filter((v) => v.type === AccessItemType.Member) .map(convertToSelectionView); - collectionView.defaultUserCollectionEmail = this.collection.defaultUserCollectionEmail; + collectionView.defaultUserCollectionEmail = this.collection?.defaultUserCollectionEmail; const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); From fcc2bc96d1779110fa8b16e374f9ace6dcbaf038 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:03:44 -0500 Subject: [PATCH 05/19] [PM-21024] Use Server for Password Change URLs (#14912) * migrate change login password service to use bitwarden server rather than fetch directly - avoids CSP entirely * add `HelpUsersUpdatePasswords` policy to policy type * add `HelpUsersUpdatePasswordsPolicy` components * allow list description override for policy description * add `HelpUsersUpdatePasswordsPolicy` when the feature flag is enabled * apply `HelpUsersUpdatePasswords` to everyone in an org * use policy to guard the well known password API * fix tests * refactor to use `policyAppliesToUser$` * remove policy work for change password - this was removed from scope * update copy for show favicon setting - it now handles both favicons and change password urls * remove favicon setting description - no longer needed * only call change password service when the setting is enabled * add popover for permitting cipher details * import permit popover directly into the settings component * replace `nativeFetch` with `fetch` * use string literal to construct URL rather than `URL` class - The `getIconsUrl` can return with an appended path which the new URL constructor will strip when passed as the base parameter * use string literal to construct URL rather than `URL` class instance (#16045) - The `getIconsUrl` can return with an appended path which the new URL constructor will strip when passed as the base parameter * [PM-24716] UI changes for Change URI work (#16043) * use platform service to launch the URI - this allows desktop to open a separate browser instance rather than use electron * fix spacing on web app * add bitLink for focus/hover states * remove spacing around links --- apps/browser/src/_locales/en/messages.json | 18 +-- .../settings/appearance-v2.component.html | 5 +- .../popup/settings/appearance-v2.component.ts | 2 + .../src/app/accounts/settings.component.html | 7 +- .../src/app/accounts/settings.component.ts | 2 + apps/desktop/src/app/app.module.ts | 1 - apps/desktop/src/locales/en/messages.json | 13 +- .../app/settings/preferences.component.html | 28 ++-- .../src/app/settings/preferences.component.ts | 8 +- apps/web/src/locales/en/messages.json | 13 +- .../response/change-password-uri.response.ts | 10 ++ ...rmit-cipher-details-popover.component.html | 22 +++ ...permit-cipher-details-popover.component.ts | 19 +++ libs/vault/src/index.ts | 1 + ...ault-change-login-password.service.spec.ts | 143 ++++++++---------- .../default-change-login-password.service.ts | 93 +++++------- 16 files changed, 205 insertions(+), 180 deletions(-) create mode 100644 libs/common/src/vault/models/response/change-password-uri.response.ts create mode 100644 libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html create mode 100644 libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 8390bc59633..00e83004016 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1760,14 +1760,8 @@ "popupU2fCloseMessage": { "message": "This browser cannot process U2F requests in this popup window. Do you want to open this popup in a new window so that you can log in using U2F?" }, - "enableFavicon": { - "message": "Show website icons" - }, - "faviconDesc": { - "message": "Show a recognizable image next to each login." - }, - "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "showIconsChangePasswordUrls": { + "message": "Show website icons and retrieve change password URLs" }, "enableBadgeCounter": { "message": "Show badge counter" @@ -4376,7 +4370,7 @@ }, "uriMatchDefaultStrategyHint": { "message": "URI match detection is how Bitwarden identifies autofill suggestions.", - "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." + "description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item." }, "regExAdvancedOptionWarning": { "message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.", @@ -5563,6 +5557,12 @@ "message": "Easily create strong and unique passwords by clicking on the Generate password button to help you keep your logins secure.", "description": "Aria label for the body content of the generator nudge" }, + "aboutThisSetting": { + "message": "About this setting" + }, + "permitCipherDetailsDescription": { + "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + }, "noPermissionsViewPage": { "message": "You do not have permissions to view this page. Try logging in with a different account." }, diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.html b/apps/browser/src/vault/popup/settings/appearance-v2.component.html index 4f7f2757e0e..c9598c76db0 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.html +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.html @@ -45,7 +45,10 @@ - {{ "enableFavicon" | i18n }} + + {{ "showIconsChangePasswordUrls" | i18n }} + + diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts index d998ef846d2..23a609bd008 100644 --- a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts @@ -23,6 +23,7 @@ import { Option, SelectModule, } from "@bitwarden/components"; +import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { PopupWidthOption } from "../../../platform/browser/browser-popup-utils"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; @@ -46,6 +47,7 @@ import { VaultPopupCopyButtonsService } from "../services/vault-popup-copy-butto ReactiveFormsModule, CheckboxModule, BadgeModule, + PermitCipherDetailsPopoverComponent, ], }) export class AppearanceV2Component implements OnInit { diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index a9fdcd2f088..091864e59ae 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -162,14 +162,15 @@ - {{ "enableFavicon" | i18n }} + {{ "showIconsChangePasswordUrls" | i18n }} +
+ +
- {{ "faviconDesc" | i18n }} diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index fd17873a4b1..a6948c689cd 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -52,6 +52,7 @@ import { TypographyModule, } from "@bitwarden/components"; import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management"; +import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { SetPinComponent } from "../../auth/components/set-pin.component"; import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting"; @@ -81,6 +82,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man SelectModule, TypographyModule, VaultTimeoutInputComponent, + PermitCipherDetailsPopoverComponent, ], }) export class SettingsComponent implements OnInit, OnDestroy { diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 014e29555e8..4f53e587994 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -31,7 +31,6 @@ import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ BrowserAnimationsModule, - SharedModule, AppRoutingModule, VaultFilterModule, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index eaa5f7f0f1e..c805096189b 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1305,11 +1305,8 @@ "message": "Automatically clear copied values from your clipboard.", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, - "enableFavicon": { - "message": "Show website icons" - }, - "faviconDesc": { - "message": "Show a recognizable image next to each login." + "showIconsChangePasswordUrls": { + "message": "Show website icons and retrieve change password URLs" }, "enableMinToTray": { "message": "Minimize to tray icon" @@ -3928,6 +3925,12 @@ "description": "Two part message", "example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent" }, + "aboutThisSetting": { + "message": "About this setting" + }, + "permitCipherDetailsDescription": { + "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + }, "assignToCollections": { "message": "Assign to collections" }, diff --git a/apps/web/src/app/settings/preferences.component.html b/apps/web/src/app/settings/preferences.component.html index 80261ecccb7..050d7395caf 100644 --- a/apps/web/src/app/settings/preferences.component.html +++ b/apps/web/src/app/settings/preferences.component.html @@ -67,23 +67,17 @@ {{ "languageDesc" | i18n }} - - - {{ "enableFavicon" | i18n }} - - - - - {{ "faviconDesc" | i18n }} - +
+ + + + {{ "showIconsChangePasswordUrls" | i18n }} + + +
+ +
+
{{ "theme" | i18n }} diff --git a/apps/web/src/app/settings/preferences.component.ts b/apps/web/src/app/settings/preferences.component.ts index e6cc35903a7..58a072ce76a 100644 --- a/apps/web/src/app/settings/preferences.component.ts +++ b/apps/web/src/app/settings/preferences.component.ts @@ -34,6 +34,7 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { DialogService } from "@bitwarden/components"; +import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault"; import { HeaderModule } from "../layouts/header/header.module"; import { SharedModule } from "../shared"; @@ -41,7 +42,12 @@ import { SharedModule } from "../shared"; @Component({ selector: "app-preferences", templateUrl: "preferences.component.html", - imports: [SharedModule, HeaderModule, VaultTimeoutInputComponent], + imports: [ + SharedModule, + HeaderModule, + VaultTimeoutInputComponent, + PermitCipherDetailsPopoverComponent, + ], }) export class PreferencesComponent implements OnInit, OnDestroy { // For use in template diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index af80433fce7..e3856e7d645 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2103,11 +2103,8 @@ "languageDesc": { "message": "Change the language used by the web vault." }, - "enableFavicon": { - "message": "Show website icons" - }, - "faviconDesc": { - "message": "Show a recognizable image next to each login." + "showIconsChangePasswordUrls": { + "message": "Show website icons and retrieve change password URLs" }, "default": { "message": "Default" @@ -10986,6 +10983,12 @@ "message": "Billing address required to add credit.", "description": "Error message shown when trying to add credit to a trialing organization without a billing address." }, + "aboutThisSetting": { + "message": "About this setting" + }, + "permitCipherDetailsDescription": { + "message": "Bitwarden will use saved login URIs to identify which icon or change password URL should be used to improve your experience. No information is collected or saved when you use this service." + }, "billingAddress": { "message": "Billing address" }, diff --git a/libs/common/src/vault/models/response/change-password-uri.response.ts b/libs/common/src/vault/models/response/change-password-uri.response.ts new file mode 100644 index 00000000000..1ff3424a269 --- /dev/null +++ b/libs/common/src/vault/models/response/change-password-uri.response.ts @@ -0,0 +1,10 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class ChangePasswordUriResponse extends BaseResponse { + uri: string | null; + + constructor(response: any) { + super(response); + this.uri = this.getResponseProperty("uri"); + } +} diff --git a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html new file mode 100644 index 00000000000..1833a148616 --- /dev/null +++ b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.html @@ -0,0 +1,22 @@ + + + +

+ {{ "permitCipherDetailsDescription" | i18n }} +

+ +
diff --git a/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts new file mode 100644 index 00000000000..8e80ddf7810 --- /dev/null +++ b/libs/vault/src/components/permit-cipher-details-popover/permit-cipher-details-popover.component.ts @@ -0,0 +1,19 @@ +import { Component, inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { LinkModule, PopoverModule } from "@bitwarden/components"; + +@Component({ + selector: "vault-permit-cipher-details-popover", + templateUrl: "./permit-cipher-details-popover.component.html", + imports: [PopoverModule, JslibModule, LinkModule], +}) +export class PermitCipherDetailsPopoverComponent { + private platformUtilService = inject(PlatformUtilsService); + + openLearnMore(e: Event) { + e.preventDefault(); + this.platformUtilService.launchUri("https://bitwarden.com/help/website-icons/"); + } +} diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index f3925ac3379..efaefc77ade 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -20,6 +20,7 @@ export { openPasswordHistoryDialog } from "./components/password-history/passwor export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component"; export * from "./components/carousel"; export * from "./components/new-cipher-menu/new-cipher-menu.component"; +export * from "./components/permit-cipher-details-popover/permit-cipher-details-popover.component"; export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service"; export { SshImportPromptService } from "./services/ssh-import-prompt.service"; diff --git a/libs/vault/src/services/default-change-login-password.service.spec.ts b/libs/vault/src/services/default-change-login-password.service.spec.ts index c9628797f4d..42242f2e4a8 100644 --- a/libs/vault/src/services/default-change-login-password.service.spec.ts +++ b/libs/vault/src/services/default-change-login-password.service.spec.ts @@ -4,10 +4,14 @@ */ import { mock } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { ClientType } from "@bitwarden/common/enums"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + Environment, + EnvironmentService, +} from "@bitwarden/common/platform/abstractions/environment.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; @@ -18,37 +22,30 @@ import { DefaultChangeLoginPasswordService } from "./default-change-login-passwo describe("DefaultChangeLoginPasswordService", () => { let service: DefaultChangeLoginPasswordService; - let mockShouldNotExistResponse: Response; - let mockWellKnownResponse: Response; - - const getClientType = jest.fn(() => ClientType.Browser); - const mockApiService = mock(); - const platformUtilsService = mock({ - getClientType, - }); + const mockDomainSettingsService = mock(); + + const showFavicons$ = new BehaviorSubject(true); beforeEach(() => { - mockApiService.nativeFetch.mockClear(); + mockApiService.fetch.mockClear(); + mockApiService.fetch.mockImplementation(() => + Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response), + ); - // Default responses to success state - mockShouldNotExistResponse = new Response("Not Found", { status: 404 }); - mockWellKnownResponse = new Response("OK", { status: 200 }); + mockDomainSettingsService.showFavicons$ = showFavicons$; - mockApiService.nativeFetch.mockImplementation((request) => { - if ( - request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200") - ) { - return Promise.resolve(mockShouldNotExistResponse); - } + const mockEnvironmentService = { + environment$: of({ + getIconsUrl: () => "https://icons.bitwarden.com", + } as Environment), + } as EnvironmentService; - if (request.url.endsWith(".well-known/change-password")) { - return Promise.resolve(mockWellKnownResponse); - } - - throw new Error("Unexpected request"); - }); - service = new DefaultChangeLoginPasswordService(mockApiService, platformUtilsService); + service = new DefaultChangeLoginPasswordService( + mockApiService, + mockEnvironmentService, + mockDomainSettingsService, + ); }); it("should return null for non-login ciphers", async () => { @@ -85,7 +82,7 @@ describe("DefaultChangeLoginPasswordService", () => { expect(url).toBeNull(); }); - it("should check the origin for a reliable status code", async () => { + it("should call the icons url endpoint", async () => { const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { @@ -95,35 +92,42 @@ describe("DefaultChangeLoginPasswordService", () => { await service.getChangePasswordUrl(cipher); - expect(mockApiService.nativeFetch).toHaveBeenCalledWith( + expect(mockApiService.fetch).toHaveBeenCalledWith( expect.objectContaining({ - url: "https://example.com/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200", + url: "https://icons.bitwarden.com/change-password-uri?uri=https%3A%2F%2Fexample.com%2F", }), ); }); - it("should attempt to fetch the well-known change password URL", async () => { + it("should return the original URI when unable to verify the response", async () => { + mockApiService.fetch.mockImplementation(() => + Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response), + ); + const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com" }], + uris: [{ uri: "https://example.com/" }], }), } as CipherView; - await service.getChangePasswordUrl(cipher); + const url = await service.getChangePasswordUrl(cipher); - expect(mockApiService.nativeFetch).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://example.com/.well-known/change-password", - }), - ); + expect(url).toBe("https://example.com/"); }); - it("should return the well-known change password URL when successful at verifying the response", async () => { + it("should return the well known change url from the response", async () => { + mockApiService.fetch.mockImplementation(() => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ uri: "https://example.com/.well-known/change-password" }), + } as Response); + }); + const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com" }], + uris: [{ uri: "https://example.com/" }, { uri: "https://working.com/" }], }), } as CipherView; @@ -132,49 +136,20 @@ describe("DefaultChangeLoginPasswordService", () => { expect(url).toBe("https://example.com/.well-known/change-password"); }); - it("should return the original URI when unable to verify the response", async () => { - mockShouldNotExistResponse = new Response("Ok", { status: 200 }); - - const cipher = { - type: CipherType.Login, - login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com/" }], - }), - } as CipherView; - - const url = await service.getChangePasswordUrl(cipher); - - expect(url).toBe("https://example.com/"); - }); - - it("should return the original URI when the well-known URL is not found", async () => { - mockWellKnownResponse = new Response("Not Found", { status: 404 }); - - const cipher = { - type: CipherType.Login, - login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com/" }], - }), - } as CipherView; - - const url = await service.getChangePasswordUrl(cipher); - - expect(url).toBe("https://example.com/"); - }); - it("should try the next URI if the first one fails", async () => { - mockApiService.nativeFetch.mockImplementation((request) => { - if ( - request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200") - ) { - return Promise.resolve(mockShouldNotExistResponse); + mockApiService.fetch.mockImplementation((request) => { + if (request.url.includes("no-wellknown.com")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ uri: null }), + } as Response); } - if (request.url.endsWith(".well-known/change-password")) { - if (request.url.includes("working.com")) { - return Promise.resolve(mockWellKnownResponse); - } - return Promise.resolve(new Response("Not Found", { status: 404 })); + if (request.url.includes("working.com")) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ uri: "https://working.com/.well-known/change-password" }), + } as Response); } throw new Error("Unexpected request"); @@ -192,19 +167,19 @@ describe("DefaultChangeLoginPasswordService", () => { expect(url).toBe("https://working.com/.well-known/change-password"); }); - it("should return the first URI when the client type is not browser", async () => { - getClientType.mockReturnValue(ClientType.Web); + it("returns the first URI when `showFavicons$` setting is disabled", async () => { + showFavicons$.next(false); const cipher = { type: CipherType.Login, login: Object.assign(new LoginView(), { - uris: [{ uri: "https://example.com/" }, { uri: "https://example-2.com/" }], + uris: [{ uri: "https://example.com/" }, { uri: "https://another.com/" }], }), } as CipherView; const url = await service.getChangePasswordUrl(cipher); - expect(mockApiService.nativeFetch).not.toHaveBeenCalled(); expect(url).toBe("https://example.com/"); + expect(mockApiService.fetch).not.toHaveBeenCalled(); }); }); diff --git a/libs/vault/src/services/default-change-login-password.service.ts b/libs/vault/src/services/default-change-login-password.service.ts index a0b5646c5a9..929f5819c02 100644 --- a/libs/vault/src/services/default-change-login-password.service.ts +++ b/libs/vault/src/services/default-change-login-password.service.ts @@ -1,9 +1,12 @@ import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { ChangePasswordUriResponse } from "@bitwarden/common/vault/models/response/change-password-uri.response"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ChangeLoginPasswordService } from "../abstractions/change-login-password.service"; @@ -12,7 +15,8 @@ import { ChangeLoginPasswordService } from "../abstractions/change-login-passwor export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordService { constructor( private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, + private environmentService: EnvironmentService, + private domainSettingsService: DomainSettingsService, ) {} /** @@ -33,24 +37,19 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer return null; } - // CSP policies on the web and desktop restrict the application from making - // cross-origin requests, breaking the below .well-known URL checks. - // For those platforms, this will short circuit and return the first URL. - // PM-21024 will build a solution for the server side to handle this. - if (this.platformUtilsService.getClientType() !== "browser") { + const enableFaviconChangePassword = await firstValueFrom( + this.domainSettingsService.showFavicons$, + ); + + // When the setting is not enabled, return the first URL + if (!enableFaviconChangePassword) { return urls[0].href; } for (const url of urls) { - const [reliable, wellKnownChangeUrl] = await Promise.all([ - this.hasReliableHttpStatusCode(url.origin), - this.getWellKnownChangePasswordUrl(url.origin), - ]); + const wellKnownChangeUrl = await this.fetchWellKnownChangePasswordUri(url.href); - // Some servers return a 200 OK for a resource that should not exist - // Which means we cannot trust the well-known URL is valid, so we skip it - // to avoid potentially sending users to a 404 page - if (reliable && wellKnownChangeUrl != null) { + if (wellKnownChangeUrl) { return wellKnownChangeUrl; } } @@ -60,55 +59,41 @@ export class DefaultChangeLoginPasswordService implements ChangeLoginPasswordSer } /** - * Checks if the server returns a non-200 status code for a resource that should not exist. - * See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics - * @param urlOrigin The origin of the URL to check + * Fetches the well-known change-password-uri for the given URL. + * @returns The full URL to the change password page, or null if it could not be found. */ - private async hasReliableHttpStatusCode(urlOrigin: string): Promise { - try { - const url = new URL( - "./.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200", - urlOrigin, - ); + private async fetchWellKnownChangePasswordUri(url: string): Promise { + const getChangePasswordUriRequest = await this.buildChangePasswordUriRequest(url); - const request = new Request(url, { - method: "GET", - mode: "same-origin", - credentials: "omit", - cache: "no-store", - redirect: "follow", - }); + const response = await this.apiService.fetch(getChangePasswordUriRequest); - const response = await this.apiService.nativeFetch(request); - return !response.ok; - } catch { - return false; + if (!response.ok) { + return null; } + + const data = await response.json(); + + const { uri } = new ChangePasswordUriResponse(data); + + return uri; } /** - * Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response - * is returned. Returns null if the request throws or the response is not 200 OK. - * See https://w3c.github.io/webappsec-change-password-url/ - * @param urlOrigin The origin of the URL to check + * Construct the request for the change-password-uri endpoint. */ - private async getWellKnownChangePasswordUrl(urlOrigin: string): Promise { - try { - const url = new URL("./.well-known/change-password", urlOrigin); + private async buildChangePasswordUriRequest(cipherUri: string): Promise { + const searchParams = new URLSearchParams(); + searchParams.set("uri", cipherUri); - const request = new Request(url, { - method: "GET", - mode: "same-origin", - credentials: "omit", - cache: "no-store", - redirect: "follow", - }); + // The change-password-uri endpoint lives within the icons service + // as it uses decrypted cipher data. + const env = await firstValueFrom(this.environmentService.environment$); + const iconsUrl = env.getIconsUrl(); - const response = await this.apiService.nativeFetch(request); + const url = new URL(`${iconsUrl}/change-password-uri?${searchParams.toString()}`); - return response.ok ? url.toString() : null; - } catch { - return null; - } + return new Request(url, { + method: "GET", + }); } } From 4c960906fa2a3d18cd43a7875082ad6ce6665cda Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:32:18 +0200 Subject: [PATCH 06/19] Account Recovery with Key Connector enabled not working prior to removal of Master Password (#15616) --- libs/angular/src/auth/guards/auth.guard.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index 8e8e70a6d29..37c464a804d 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -74,13 +74,6 @@ export const authGuard: CanActivateFn = async ( return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } }); } - if ( - !routerState.url.includes("remove-password") && - (await firstValueFrom(keyConnectorService.convertAccountRequired$)) - ) { - return router.createUrlTree(["/remove-password"]); - } - // Handle cases where a user needs to set a password when they don't already have one: // - TDE org user has been given "manage account recovery" permission // - TDE offboarding on a trusted device, where we have access to their encryption key wrap with their new password @@ -106,5 +99,14 @@ export const authGuard: CanActivateFn = async ( return router.createUrlTree([route]); } + // Remove password when Key Connector is enabled + if ( + forceSetPasswordReason == ForceSetPasswordReason.None && + !routerState.url.includes("remove-password") && + (await firstValueFrom(keyConnectorService.convertAccountRequired$)) + ) { + return router.createUrlTree(["/remove-password"]); + } + return true; }; From 5f7c0ae999970dbbb05a111d074eb20a7258da15 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 27 Aug 2025 11:56:42 -0400 Subject: [PATCH 07/19] build: ensure new libraries are added to the root jest.config (#16166) * Add missing libs to jest.config.js Added 15 missing libraries to the jest projects array: - libs/assets - libs/client-type - libs/core-test-utils - libs/dirt/card - libs/guid - libs/logging - libs/messaging-internal - libs/messaging - libs/serialization - libs/state-test-utils - libs/state - libs/storage-core - libs/storage-test-utils - libs/tools/export/vault-export/vault-export-ui - libs/user-core This ensures all existing libraries with jest.config.js files are included in CI test runs. * Update basic-lib generator to add new libs to jest.config.js - Added updateJestConfig function that automatically adds new libraries to jest.config.js - Function finds the appropriate alphabetical position for the new library - Added comprehensive tests for the new functionality - Ensures new libraries are included in CI test runs from creation This prevents the issue where new libraries are created but their tests are not run in CI because they are missing from the jest configuration. * Fix import statements in state-definitions and deserialization-helpers tests - Fixed ClientLocations import in state-definitions.spec.ts to use @bitwarden/storage-core instead of relative import - Simplified deserialization-helpers.spec.ts import to use library root @bitwarden/serialization --- jest.config.js | 25 +++++-- .../src/generators/basic-lib.spec.ts | 41 ++++++++++++ libs/nx-plugin/src/generators/basic-lib.ts | 65 +++++++++++++++++++ .../src/deserialization-helpers.spec.ts | 2 +- libs/state/src/core/state-definitions.spec.ts | 4 +- 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/jest.config.js b/jest.config.js index b0ffd2382ca..3e4fb665a8c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,24 +26,39 @@ module.exports = { "/libs/admin-console/jest.config.js", "/libs/angular/jest.config.js", + "/libs/assets/jest.config.js", "/libs/auth/jest.config.js", "/libs/billing/jest.config.js", + "/libs/client-type/jest.config.js", "/libs/common/jest.config.js", "/libs/components/jest.config.js", + "/libs/core-test-utils/jest.config.js", + "/libs/dirt/card/jest.config.js", "/libs/eslint/jest.config.js", + "/libs/guid/jest.config.js", + "/libs/importer/jest.config.js", + "/libs/key-management/jest.config.js", + "/libs/key-management-ui/jest.config.js", + "/libs/logging/jest.config.js", + "/libs/messaging-internal/jest.config.js", + "/libs/messaging/jest.config.js", + "/libs/node/jest.config.js", + "/libs/platform/jest.config.js", + "/libs/serialization/jest.config.js", + "/libs/state-test-utils/jest.config.js", + "/libs/state/jest.config.js", + "/libs/storage-core/jest.config.js", + "/libs/storage-test-utils/jest.config.js", "/libs/tools/export/vault-export/vault-export-core/jest.config.js", + "/libs/tools/export/vault-export/vault-export-ui/jest.config.js", "/libs/tools/generator/core/jest.config.js", "/libs/tools/generator/components/jest.config.js", "/libs/tools/generator/extensions/history/jest.config.js", "/libs/tools/generator/extensions/legacy/jest.config.js", "/libs/tools/generator/extensions/navigation/jest.config.js", "/libs/tools/send/send-ui/jest.config.js", - "/libs/importer/jest.config.js", - "/libs/platform/jest.config.js", - "/libs/node/jest.config.js", + "/libs/user-core/jest.config.js", "/libs/vault/jest.config.js", - "/libs/key-management/jest.config.js", - "/libs/key-management-ui/jest.config.js", ], // Workaround for a memory leak that crashes tests in CI: diff --git a/libs/nx-plugin/src/generators/basic-lib.spec.ts b/libs/nx-plugin/src/generators/basic-lib.spec.ts index a0357ca1751..9fd7a702375 100644 --- a/libs/nx-plugin/src/generators/basic-lib.spec.ts +++ b/libs/nx-plugin/src/generators/basic-lib.spec.ts @@ -83,3 +83,44 @@ describe("basic-lib generator", () => { expect(tree.exists(`libs/test/src/test.spec.ts`)).toBeTruthy(); }); }); + +it("should update jest.config.js with new library", async () => { + // Create a mock jest.config.js with existing libs + const existingJestConfig = `module.exports = { + projects: [ + "/apps/browser/jest.config.js", + "/libs/admin-console/jest.config.js", + "/libs/auth/jest.config.js", + "/libs/vault/jest.config.js", + ], +};`; + tree.write("jest.config.js", existingJestConfig); + + await basicLibGenerator(tree, options); + + const jestConfigContent = tree.read("jest.config.js"); + expect(jestConfigContent).not.toBeNull(); + const jestConfig = jestConfigContent?.toString(); + + // Should contain the new library in alphabetical order + expect(jestConfig).toContain('"/libs/test/jest.config.js",'); + + // Should be in the right alphabetical position (after auth, before vault) + const authIndex = jestConfig?.indexOf('"/libs/auth/jest.config.js"'); + const testIndex = jestConfig?.indexOf('"/libs/test/jest.config.js"'); + const vaultIndex = jestConfig?.indexOf('"/libs/vault/jest.config.js"'); + + expect(authIndex).toBeDefined(); + expect(testIndex).toBeDefined(); + expect(vaultIndex).toBeDefined(); + expect(authIndex! < testIndex!).toBeTruthy(); + expect(testIndex! < vaultIndex!).toBeTruthy(); +}); + +it("should handle missing jest.config.js file gracefully", async () => { + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + // Don't create jest.config.js file + await basicLibGenerator(tree, options); + expect(consoleSpy).toHaveBeenCalledWith("jest.config.js file not found at root"); + consoleSpy.mockRestore(); +}); diff --git a/libs/nx-plugin/src/generators/basic-lib.ts b/libs/nx-plugin/src/generators/basic-lib.ts index 6b214d18921..4f2f542ac89 100644 --- a/libs/nx-plugin/src/generators/basic-lib.ts +++ b/libs/nx-plugin/src/generators/basic-lib.ts @@ -53,6 +53,9 @@ export async function basicLibGenerator( // Update CODEOWNERS with the new lib updateCodeowners(tree, options.directory, options.name, options.team); + // Update jest.config.js with the new lib + updateJestConfig(tree, options.directory, options.name); + // Format all new files with prettier await formatFiles(tree); @@ -124,4 +127,66 @@ function updateCodeowners(tree: Tree, directory: string, name: string, team: str tree.write(codeownersPath, content + newLine); } +/** + * Updates the jest.config.js file to include the new library + * This ensures the library's tests are included in CI runs + * + * @param {Tree} tree - The virtual file system tree + * @param {string} directory - Directory where the library is created + * @param {string} name - The library name + */ +function updateJestConfig(tree: Tree, directory: string, name: string) { + const jestConfigPath = "jest.config.js"; + + if (!tree.exists(jestConfigPath)) { + console.warn("jest.config.js file not found at root"); + return; + } + + const content = tree.read(jestConfigPath)?.toString() || ""; + const libJestPath = `"/${directory}/${name}/jest.config.js",`; + + // Find the libs section and insert the new library in alphabetical order + const lines = content.split("\n"); + let insertIndex = -1; + let foundLibsSection = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check if we're in the libs section + if (line.includes('"/libs/')) { + foundLibsSection = true; + + // Extract the lib name for comparison + const match = line.match(/"libs([^"]+)/); + if (match) { + const existingLibName = match[1]; + + // If the new lib should come before this existing lib alphabetically + if (name < existingLibName) { + insertIndex = i; + break; + } + } + } + // If we were in libs section but hit a non-libs line, insert at end of libs + else if (foundLibsSection && !line.includes('"/libs/')) { + insertIndex = i; + break; + } + } + + if (insertIndex === -1) { + console.warn(`Could not find appropriate location to insert ${name} in jest.config.js`); + return; + } + + // Insert the new library line + lines.splice(insertIndex, 0, ` ${libJestPath}`); + + // Write back the updated content + tree.write(jestConfigPath, lines.join("\n")); +} + export default basicLibGenerator; diff --git a/libs/serialization/src/deserialization-helpers.spec.ts b/libs/serialization/src/deserialization-helpers.spec.ts index 1918673c8d2..0c4bd2f06eb 100644 --- a/libs/serialization/src/deserialization-helpers.spec.ts +++ b/libs/serialization/src/deserialization-helpers.spec.ts @@ -1,4 +1,4 @@ -import { record } from "@bitwarden/serialization/deserialization-helpers"; +import { record } from "@bitwarden/serialization"; describe("deserialization helpers", () => { describe("record", () => { diff --git a/libs/state/src/core/state-definitions.spec.ts b/libs/state/src/core/state-definitions.spec.ts index d0e6eb3082e..92b3f049a0c 100644 --- a/libs/state/src/core/state-definitions.spec.ts +++ b/libs/state/src/core/state-definitions.spec.ts @@ -1,4 +1,6 @@ -import { ClientLocations, StateDefinition } from "./state-definition"; +import { ClientLocations } from "@bitwarden/storage-core"; + +import { StateDefinition } from "./state-definition"; import * as stateDefinitionsRecord from "./state-definitions"; describe.each(["web", "cli", "desktop", "browser"])( From 38f62a01499b46d4ba9d9ddfeeffce2695645d12 Mon Sep 17 00:00:00 2001 From: Vicki League Date: Wed, 27 Aug 2025 12:40:16 -0400 Subject: [PATCH 08/19] [PM-25222] Fix svg alignment issues caused by new preflight defaults (#16181) --- libs/components/src/tw-theme-preflight.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/components/src/tw-theme-preflight.css b/libs/components/src/tw-theme-preflight.css index 3c38eba3bd3..e5f35885993 100644 --- a/libs/components/src/tw-theme-preflight.css +++ b/libs/components/src/tw-theme-preflight.css @@ -65,4 +65,9 @@ select { appearance: none; } + + /* overriding preflight since the apps were built assuming svgs are inline */ + svg { + display: inline; + } } From 4dd7e0cafa0b71ddc741c7191e7d51557fc2fe25 Mon Sep 17 00:00:00 2001 From: Alex <55413326+AlexRubik@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:41:42 -0700 Subject: [PATCH 09/19] Messages: unmark critical (#16146) * Unmark as critical * Revert "Unmark as critical" This reverts commit 3fa79fc4988efefb9e2749ab04a4a60c75930246. * rename unmarkAsCriticalApp -> unmarkAsCritical --- apps/web/src/locales/en/messages.json | 4 ++-- .../app-table-row-scrollable.component.html | 4 ++-- .../access-intelligence/app-table-row-scrollable.component.ts | 2 +- .../access-intelligence/critical-applications.component.html | 2 +- .../access-intelligence/critical-applications.component.ts | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index e3856e7d645..7de7f119e3b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -176,8 +176,8 @@ "totalApplications": { "message": "Total applications" }, - "unmarkAsCriticalApp": { - "message": "Unmark as critical app" + "unmarkAsCritical": { + "message": "Unmark as critical" }, "criticalApplicationSuccessfullyUnmarked": { "message": "Critical application successfully unmarked" diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html index 8e48b5e107d..eb7544faf05 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.html @@ -88,8 +88,8 @@ > - diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts index a729f21158f..c6923bf5c77 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/app-table-row-scrollable.component.ts @@ -19,6 +19,6 @@ export class AppTableRowScrollableComponent { @Input() selectedUrls: Set = new Set(); @Input() isDrawerIsOpenForThisRecord!: (applicationName: string) => boolean; @Input() showAppAtRiskMembers!: (applicationName: string) => void; - @Input() unmarkAsCriticalApp!: (applicationName: string) => void; + @Input() unmarkAsCritical!: (applicationName: string) => void; @Input() checkboxChange!: (applicationName: string, $event: Event) => void; } diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html index ffef3f3b0b9..17967ccef0e 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.html @@ -83,6 +83,6 @@ [showRowMenuForCriticalApps]="true" [isDrawerIsOpenForThisRecord]="isDrawerOpenForTableRow" [showAppAtRiskMembers]="showAppAtRiskMembers" - [unmarkAsCriticalApp]="unmarkAsCriticalApp" + [unmarkAsCritical]="unmarkAsCritical" > diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts index 531adc003c7..58e0f648749 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/critical-applications.component.ts @@ -106,7 +106,7 @@ export class CriticalApplicationsComponent implements OnInit { ); }; - unmarkAsCriticalApp = async (hostname: string) => { + unmarkAsCritical = async (hostname: string) => { try { await this.criticalAppsService.dropCriticalApp( this.organizationId as OrganizationId, From 1d5115f19050d81d58723b435581188928809bf0 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:51:02 -0400 Subject: [PATCH 10/19] Making accessing a project only use one api call to getbyprojectid (#15854) --- .../guards/project-access.guard.spec.ts | 135 ------------------ .../projects/guards/project-access.guard.ts | 33 ----- .../project/project-secrets.component.html | 8 +- .../project/project-secrets.component.ts | 33 +---- .../projects/project/project.component.html | 2 +- .../projects/projects-routing.module.ts | 2 - 6 files changed, 12 insertions(+), 201 deletions(-) delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts deleted file mode 100644 index 7523fa14a21..00000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Component } from "@angular/core"; -import { TestBed } from "@angular/core/testing"; -import { Router } from "@angular/router"; -import { RouterTestingModule } from "@angular/router/testing"; -import { MockProxy, mock } from "jest-mock-extended"; -import { of } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { RouterService } from "@bitwarden/web-vault/app/core"; - -import { ProjectView } from "../../models/view/project.view"; -import { ProjectService } from "../project.service"; - -import { projectAccessGuard } from "./project-access.guard"; - -@Component({ - template: "", - standalone: false, -}) -export class GuardedRouteTestComponent {} - -@Component({ - template: "", - standalone: false, -}) -export class RedirectTestComponent {} - -describe("Project Redirect Guard", () => { - let organizationService: MockProxy; - let routerService: MockProxy; - let projectServiceMock: MockProxy; - let i18nServiceMock: MockProxy; - let toastService: MockProxy; - let router: Router; - let accountService: FakeAccountService; - const userId = Utils.newGuid() as UserId; - - const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization; - const projectView = { - id: "123", - organizationId: "123", - name: "project-name", - creationDate: Date.now.toString(), - revisionDate: Date.now.toString(), - read: true, - write: true, - } as ProjectView; - - beforeEach(async () => { - organizationService = mock(); - routerService = mock(); - projectServiceMock = mock(); - i18nServiceMock = mock(); - toastService = mock(); - accountService = mockAccountServiceWith(userId); - - TestBed.configureTestingModule({ - imports: [ - RouterTestingModule.withRoutes([ - { - path: "sm/:organizationId/projects/:projectId", - component: GuardedRouteTestComponent, - canActivate: [projectAccessGuard], - }, - { - path: "sm", - component: RedirectTestComponent, - }, - { - path: "sm/:organizationId/projects", - component: RedirectTestComponent, - }, - ]), - ], - providers: [ - { provide: OrganizationService, useValue: organizationService }, - { provide: AccountService, useValue: accountService }, - { provide: RouterService, useValue: routerService }, - { provide: ProjectService, useValue: projectServiceMock }, - { provide: I18nService, useValue: i18nServiceMock }, - { provide: ToastService, useValue: toastService }, - ], - }); - - router = TestBed.inject(Router); - }); - - it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => { - // Arrange - organizationService.organizations$.mockReturnValue(of([smOrg1])); - projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView)); - - // Act - await router.navigateByUrl("sm/123/projects/123"); - - // Assert - expect(router.url).toBe("/sm/123/projects/123"); - }); - - it("redirects to sm/projects if project does not exist", async () => { - // Arrange - organizationService.organizations$.mockReturnValue(of([smOrg1])); - - // Act - await router.navigateByUrl("sm/123/projects/124"); - - // Assert - expect(router.url).toBe("/sm/123/projects"); - }); - - it("redirects to sm/123/projects if exception occurs while looking for Project", async () => { - // Arrange - jest.spyOn(projectServiceMock, "getByProjectId").mockImplementation(() => { - throw new Error("Test error"); - }); - jest.spyOn(i18nServiceMock, "t").mockReturnValue("Project not found"); - - // Act - await router.navigateByUrl("sm/123/projects/123"); - // Assert - expect(toastService.showToast).toHaveBeenCalledWith({ - variant: "error", - title: null, - message: "Project not found", - }); - expect(router.url).toBe("/sm/123/projects"); - }); -}); diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts deleted file mode 100644 index 2c6723a56a2..00000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/guards/project-access.guard.ts +++ /dev/null @@ -1,33 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { inject } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router"; - -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { ToastService } from "@bitwarden/components"; - -import { ProjectService } from "../project.service"; - -/** - * Redirects to projects list if the user doesn't have access to project. - */ -export const projectAccessGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => { - const projectService = inject(ProjectService); - const toastService = inject(ToastService); - const i18nService = inject(I18nService); - - try { - const project = await projectService.getByProjectId(route.params.projectId); - if (project) { - return true; - } - } catch { - toastService.showToast({ - variant: "error", - title: null, - message: i18nService.t("notFound", i18nService.t("project")), - }); - return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]); - } - return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]); -}; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index d9919ef6bac..7a2968cfb2c 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -1,7 +1,7 @@ - - + +
; private organizationEnabled: boolean; + protected project = inject(ROUTER_OUTLET_DATA) as Signal; + readonly writeAccess = computed(() => this.project().write); + readonly projectExists = computed(() => !!this.project()); constructor( private route: ActivatedRoute, - private projectService: ProjectService, private secretService: SecretService, private dialogService: DialogService, private platformUtilsService: PlatformUtilsService, @@ -68,21 +61,9 @@ export class ProjectSecretsComponent implements OnInit { ) {} ngOnInit() { - // Refresh list if project is edited - const currentProjectEdited = this.projectService.project$.pipe( - filter((p) => p?.id === this.projectId), - startWith(null), - ); - - this.project$ = combineLatest([this.route.params, currentProjectEdited]).pipe( - switchMap(([params, _]) => { - return this.projectService.getByProjectId(params.projectId); - }), - ); - this.secrets$ = this.secretService.secret$.pipe( startWith(null), - combineLatestWith(this.route.params, currentProjectEdited), + combineLatestWith(this.route.params), switchMap(async ([_, params]) => { this.organizationId = params.organizationId; this.projectId = params.projectId; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html index d44d7898cac..ef7c8ff1275 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project.component.html @@ -36,4 +36,4 @@ {{ "editProject" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts index 231486703c9..6078520989a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/projects-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { projectAccessGuard } from "./guards/project-access.guard"; import { ProjectPeopleComponent } from "./project/project-people.component"; import { ProjectSecretsComponent } from "./project/project-secrets.component"; import { ProjectServiceAccountsComponent } from "./project/project-service-accounts.component"; @@ -16,7 +15,6 @@ const routes: Routes = [ { path: ":projectId", component: ProjectComponent, - canActivate: [projectAccessGuard], children: [ { path: "", From cde4890e5eba782d46a8adc05323c72629d17c73 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Wed, 27 Aug 2025 15:34:35 -0400 Subject: [PATCH 11/19] PM-24791 [Defect] White box behind Save login notification UI (#16112) * PM-24791 * update snapshots --- .../overlay-notifications-content.service.spec.ts.snap | 2 +- .../content/overlay-notifications-content.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap index 18c3baa876c..7bdde2560d0 100644 --- a/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/notifications/content/__snapshots__/overlay-notifications-content.service.spec.ts.snap @@ -8,7 +8,7 @@ exports[`OverlayNotificationsContentService opening the notification bar creates