From d57d653551286133610d1df92b57146c60559170 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 29 Aug 2025 15:40:20 +0000 Subject: [PATCH 01/18] Bumped client version(s) --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 2aca101130d..6eb9a576a23 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2025.8.1", + "version": "2025.8.2", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index aa35f522eeb..9e65708076b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -291,7 +291,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.8.1" + "version": "2025.8.2" }, "libs/admin-console": { "name": "@bitwarden/admin-console", From e0da2671b43715a19bd2132fa4c352eb7b063bad Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 29 Aug 2025 19:09:33 +0200 Subject: [PATCH 02/18] [PM-13374] Update all SDK uuids (#14962) * fix: broken SDK interface * Fix all compile errors related to uuids * Fix browser desktop * Fix tests --------- Co-authored-by: Andreas Coroiu --- .../vault-list-items-container.component.ts | 5 ++-- .../vault-popup-items.service.spec.ts | 3 ++- .../vault-popup-list-filters.service.spec.ts | 16 ++++++++++--- .../vault-popup-list-filters.service.ts | 6 ++++- .../app/vault/vault-items-v2.component.ts | 3 ++- .../collection-name.badge.component.ts | 6 +++-- .../vault-items/vault-items.component.ts | 4 ++-- .../organization-name-badge.component.ts | 3 ++- .../pipes/get-organization-name.pipe.ts | 3 ++- .../shared/models/filter-function.ts | 2 +- .../vault/individual-vault/vault.component.ts | 24 +++++++++++-------- .../models/vault-filter.model.spec.ts | 9 ++++--- .../vault-filter/models/vault-filter.model.ts | 4 +++- .../platform/abstractions/sdk/sdk.service.ts | 2 +- .../services/sdk/default-sdk.service.spec.ts | 2 +- .../services/sdk/default-sdk.service.ts | 6 ++--- .../src/vault/models/domain/cipher.spec.ts | 18 +++++++------- libs/common/src/vault/models/domain/cipher.ts | 18 +++++++------- .../src/vault/models/view/cipher.view.spec.ts | 19 ++++++++------- .../src/vault/models/view/cipher.view.ts | 12 +++++----- .../services/cipher-authorization.service.ts | 3 ++- .../src/vault/services/cipher.service.ts | 3 ++- .../default-cipher-encryption.service.spec.ts | 14 +++++------ .../default-cipher-encryption.service.ts | 8 +++---- .../services/restricted-item-types.service.ts | 3 ++- .../src/vault/services/search.service.ts | 5 ++-- .../utils/cipher-view-like-utils.spec.ts | 4 ++-- .../components/copy-cipher-field.directive.ts | 6 ++++- .../src/services/copy-cipher-field.service.ts | 5 ++-- package-lock.json | 8 +++---- package.json | 2 +- 31 files changed, 133 insertions(+), 93 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 5a08ed3002b..61d7815d93e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -23,6 +23,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -321,7 +322,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit { } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.cipherService.updateLastLaunchedDate(cipher.id!, activeUserId); + await this.cipherService.updateLastLaunchedDate(uuidAsString(cipher.id!), activeUserId); await BrowserApi.createNewTab(launchURI); @@ -338,7 +339,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit { // When only the `CipherListView` is available, fetch the full cipher details const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const _cipher = await this.cipherService.get(cipher.id!, activeUserId); + const _cipher = await this.cipherService.get(uuidAsString(cipher.id!), activeUserId); const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); await this.vaultPopupAutofillService.doAutofill(cipherView); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 32974da162d..3ba4e832b02 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { ObservableTracker, mockAccountServiceWith } from "@bitwarden/common/spec"; @@ -102,7 +103,7 @@ describe("VaultPopupItemsService", () => { searchService.searchCiphers.mockImplementation(async (userId, _, __, ciphers) => ciphers); cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => - ciphers.filter((c) => ["0", "1"].includes(c.id)), + ciphers.filter((c) => ["0", "1"].includes(uuidAsString(c.id))), ); vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false); vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index ebaeaeb6076..eecd1f2fd68 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -14,6 +14,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { ProductTierType } from "@bitwarden/common/billing/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; @@ -514,8 +515,17 @@ describe("VaultPopupListFiltersService", () => { describe("filterFunction$", () => { const ciphers = [ { type: CipherType.Login, collectionIds: [], organizationId: null }, - { type: CipherType.Card, collectionIds: ["1234"], organizationId: "8978" }, - { type: CipherType.Identity, collectionIds: [], folderId: "5432", organizationId: null }, + { + type: CipherType.Card, + collectionIds: [asUuid("cbcae898-9f9a-48eb-863e-edf92e3ad7e0")], + organizationId: "8978" as any, + }, + { + type: CipherType.Identity, + collectionIds: [], + folderId: "5432" as any, + organizationId: null, + }, { type: CipherType.SecureNote, collectionIds: [], organizationId: null }, ] as CipherView[]; @@ -529,7 +539,7 @@ describe("VaultPopupListFiltersService", () => { }); it("filters by collection", (done) => { - const collection = { id: "1234" } as CollectionView; + const collection = { id: "cbcae898-9f9a-48eb-863e-edf92e3ad7e0" } as CollectionView; service.filterFunction$.subscribe((filterFunction) => { expect(filterFunction(ciphers)).toEqual([ciphers[1]]); diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index adc0589e7e8..05d0ea8d444 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -28,6 +28,7 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { KeyDefinition, @@ -236,7 +237,10 @@ export class VaultPopupListFiltersService { return false; } - if (filters.collection && !cipher.collectionIds?.includes(filters.collection.id!)) { + if ( + filters.collection && + !cipher.collectionIds?.includes(asUuid(filters.collection.id!)) + ) { return false; } diff --git a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts index e99e05e385a..06654fb1a5c 100644 --- a/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items-v2.component.ts @@ -7,6 +7,7 @@ import { distinctUntilChanged, debounceTime } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SearchService } from "@bitwarden/common/vault/abstractions/search.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; @@ -43,6 +44,6 @@ export class VaultItemsV2Component extends BaseVaultIt } trackByFn(index: number, c: C): string { - return c.id!; + return uuidAsString(c.id!); } } diff --git a/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts b/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts index 728faaf66e2..d3893b5bd24 100644 --- a/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/collection-badge/collection-name.badge.component.ts @@ -3,6 +3,8 @@ import { Component, Input } from "@angular/core"; import { CollectionView } from "@bitwarden/admin-console/common"; +import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { CollectionId } from "@bitwarden/sdk-internal"; import { SharedModule } from "../../../../shared/shared.module"; import { GetCollectionNameFromIdPipe } from "../pipes"; @@ -13,11 +15,11 @@ import { GetCollectionNameFromIdPipe } from "../pipes"; imports: [SharedModule, GetCollectionNameFromIdPipe], }) export class CollectionNameBadgeComponent { - @Input() collectionIds: string[]; + @Input() collectionIds: CollectionId[] | string[]; @Input() collections: CollectionView[]; get shownCollections(): string[] { - return this.showXMore ? this.collectionIds.slice(0, 2) : this.collectionIds; + return (this.showXMore ? this.collectionIds.slice(0, 2) : this.collectionIds).map(uuidAsString); } get showXMore(): boolean { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 8fde2eb44e4..a5bcb915713 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -310,7 +310,7 @@ export class VaultItemsComponent { const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id); for (const collection of orgCollections) { - if (vaultItem.cipher.collectionIds.includes(collection.id) && collection.manage) { + if (vaultItem.cipher.collectionIds.includes(collection.id as any) && collection.manage) { return true; } } @@ -364,7 +364,7 @@ export class VaultItemsComponent { } return this.allCollections - .filter((c) => cipher.collectionIds.includes(c.id)) + .filter((c) => cipher.collectionIds.includes(c.id as any)) .some((collection) => collection.manage); } diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index 915bc00bacd..79fae4d5b1f 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -8,6 +8,7 @@ import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.servic import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { OrganizationId } from "@bitwarden/sdk-internal"; @Component({ selector: "app-org-badge", @@ -15,7 +16,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; standalone: false, }) export class OrganizationNameBadgeComponent implements OnChanges { - @Input() organizationId?: string; + @Input() organizationId?: OrganizationId | string; @Input() organizationName: string; @Input() disabled: boolean; diff --git a/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts b/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts index bf9dc82c527..b80aa508f75 100644 --- a/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts +++ b/apps/web/src/app/vault/individual-vault/pipes/get-organization-name.pipe.ts @@ -1,6 +1,7 @@ import { Pipe, PipeTransform } from "@angular/core"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationId } from "@bitwarden/sdk-internal"; @Pipe({ name: "orgNameFromId", @@ -8,7 +9,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga standalone: false, }) export class GetOrgNameFromIdPipe implements PipeTransform { - transform(value: string, organizations: Organization[]) { + transform(value: string | OrganizationId, organizations: Organization[]) { const orgName = organizations?.find((o) => o.id === value)?.name; return orgName; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts index 1ed2e481fb8..7fdd0804c08 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -69,7 +69,7 @@ export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunc filter.collectionId !== undefined && filter.collectionId !== All && filter.collectionId !== Unassigned && - (cipher.collectionIds == null || !cipher.collectionIds.includes(filter.collectionId)) + (cipher.collectionIds == null || !cipher.collectionIds.includes(filter.collectionId as any)) ) { return false; } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 35520a812a8..e27847d5267 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -56,6 +56,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; @@ -678,7 +679,7 @@ export class VaultComponent implements OnInit, OnDestr return; } else if (cipher.organizationId != null) { const org = await firstValueFrom( - this.organizations$.pipe(getOrganizationById(cipher.organizationId)), + this.organizations$.pipe(getOrganizationById(uuidAsString(cipher.organizationId))), ); if (org != null && (org.maxStorageGb == null || org.maxStorageGb === 0)) { this.messagingService.send("upgradeOrganization", { @@ -775,7 +776,7 @@ export class VaultComponent implements OnInit, OnDestr } async editCipher(cipher: CipherView | CipherListView, cloneMode?: boolean) { - return this.editCipherId(cipher?.id, cloneMode); + return this.editCipherId(uuidAsString(cipher?.id), cloneMode); } /** @@ -1044,7 +1045,7 @@ export class VaultComponent implements OnInit, OnDestr try { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.cipherService.restoreWithServer(c.id, activeUserId); + await this.cipherService.restoreWithServer(uuidAsString(c.id), activeUserId); this.toastService.showToast({ variant: "success", title: null, @@ -1066,7 +1067,7 @@ export class VaultComponent implements OnInit, OnDestr return; } - const selectedCipherIds = ciphers.map((cipher) => cipher.id); + const selectedCipherIds = ciphers.map((cipher) => uuidAsString(cipher.id)); if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", @@ -1128,7 +1129,7 @@ export class VaultComponent implements OnInit, OnDestr try { const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - await this.deleteCipherWithServer(c.id, activeUserId, permanent); + await this.deleteCipherWithServer(uuidAsString(c.id), activeUserId, permanent); this.toastService.showToast({ variant: "success", @@ -1168,7 +1169,7 @@ export class VaultComponent implements OnInit, OnDestr const dialog = openBulkDeleteDialog(this.dialogService, { data: { permanent: this.filter.type === "trash", - cipherIds: ciphers.map((c) => c.id), + cipherIds: ciphers.map((c) => uuidAsString(c.id)), organizations: organizations, collections: collections, }, @@ -1185,7 +1186,7 @@ export class VaultComponent implements OnInit, OnDestr return; } - const selectedCipherIds = ciphers.map((cipher) => cipher.id); + const selectedCipherIds = ciphers.map((cipher) => uuidAsString(cipher.id)); if (selectedCipherIds.length === 0) { this.toastService.showToast({ variant: "error", @@ -1261,11 +1262,14 @@ export class VaultComponent implements OnInit, OnDestr }); if (field === "password") { - await this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id); + await this.eventCollectionService.collect( + EventType.Cipher_ClientCopiedPassword, + uuidAsString(cipher.id), + ); } else if (field === "totp") { await this.eventCollectionService.collect( EventType.Cipher_ClientCopiedHiddenField, - cipher.id, + uuidAsString(cipher.id), ); } } @@ -1324,7 +1328,7 @@ export class VaultComponent implements OnInit, OnDestr } const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const _cipher = await this.cipherService.get(cipher.id, activeUserId); + const _cipher = await this.cipherService.get(uuidAsString(cipher.id), activeUserId); const cipherView = await this.cipherService.decrypt(_cipher, activeUserId); return cipherView.login?.password; } diff --git a/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts b/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts index f5a49e25618..ea5e9eb9b24 100644 --- a/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts +++ b/libs/angular/src/vault/vault-filter/models/vault-filter.model.spec.ts @@ -118,15 +118,18 @@ describe("VaultFilter", () => { }); describe("given an organizational cipher (with organization and collections)", () => { + const collection1 = "e9652fc0-1fe4-48d5-a3d8-d821e32fbd98"; + const collection2 = "42a971a5-8c16-48a3-a725-4be27cd99bc9"; + const cipher = createCipher({ organizationId: "organizationId", - collectionIds: ["collectionId", "anotherId"], + collectionIds: [collection1, collection2], }); it("should return true when filter matches collection id", () => { const filterFunction = createFilterFunction({ selectedCollection: true, - selectedCollectionId: "collectionId", + selectedCollectionId: collection1, }); const result = filterFunction(cipher); @@ -137,7 +140,7 @@ describe("VaultFilter", () => { it("should return false when filter does not match collection id", () => { const filterFunction = createFilterFunction({ selectedCollection: true, - selectedCollectionId: "nonMatchingId", + selectedCollectionId: "1ea7ad96-3fc1-4567-8fe5-91aa9f697fd1", }); const result = filterFunction(cipher); diff --git a/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts b/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts index fa383dd28da..bade2244ff0 100644 --- a/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts +++ b/libs/angular/src/vault/vault-filter/models/vault-filter.model.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherViewLike, @@ -65,7 +66,8 @@ export class VaultFilter { } if (this.selectedCollection && this.selectedCollectionId != null && cipherPassesFilter) { cipherPassesFilter = - cipher.collectionIds != null && cipher.collectionIds.includes(this.selectedCollectionId); + cipher.collectionIds != null && + cipher.collectionIds.includes(asUuid(this.selectedCollectionId)); } if (this.selectedOrganizationId != null && cipherPassesFilter) { cipherPassesFilter = cipher.organizationId === this.selectedOrganizationId; diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index d629e4fe9fa..03baec5cc37 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -32,7 +32,7 @@ export function asUuid(uuid: string): T { /** * Converts a UUID to the string representation. */ -export function uuidToString(uuid: T): string { +export function uuidAsString(uuid: T): string { return uuid as unknown as string; } diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index 30b43bfe00a..8ebc2dcb62b 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -72,7 +72,7 @@ describe("DefaultSdkService", () => { }); describe("given the user is logged in", () => { - const userId = "user-id" as UserId; + const userId = "0da62ebd-98bb-4f42-a846-64e8555087d7" as UserId; beforeEach(() => { environmentService.getEnvironment$ .calledWith(userId) diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index 629d7dafed5..9d965acb44b 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -35,7 +35,7 @@ import { Environment, EnvironmentService } from "../../abstractions/environment. import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; -import { SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.service"; +import { asUuid, SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.service"; import { compareValues } from "../../misc/compare-values"; import { Rc } from "../../misc/reference-counting/rc"; import { StateProvider } from "../../state"; @@ -218,7 +218,7 @@ export class DefaultSdkService implements SdkService { orgKeys: Record | null, ) { await client.crypto().initialize_user_crypto({ - userId, + userId: asUuid(userId), email: account.email, method: { decryptedKey: { decrypted_user_key: userKey.keyB64 } }, kdfParams: @@ -242,7 +242,7 @@ export class DefaultSdkService implements SdkService { organizationKeys: new Map( Object.entries(orgKeys ?? {}) .filter(([_, v]) => v.type === "organization") - .map(([k, v]) => [k, v.key as UnsignedSharedKey]), + .map(([k, v]) => [asUuid(k), v.key as UnsignedSharedKey]), ), }); diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 008324f9aec..8afd6824379 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -849,9 +849,9 @@ describe("Cipher DTO", () => { const lastLaunched = new Date("2025-04-15T12:00:00.000Z").getTime(); const cipherData: CipherData = { - id: "id", - organizationId: "orgId", - folderId: "folderId", + id: "2afb03fd-0d8e-4c08-a316-18b2f0efa618", + organizationId: "4748ad12-212e-4bc8-82b7-a75f6709d033", + folderId: "b4dac811-e44a-495a-9334-9e53b7aaf54c", edit: true, permissions: new CipherPermissionsApi(), viewPassword: true, @@ -920,9 +920,9 @@ describe("Cipher DTO", () => { const sdkCipher = cipher.toSdkCipher(); expect(sdkCipher).toEqual({ - id: "id", - organizationId: "orgId", - folderId: "folderId", + id: "2afb03fd-0d8e-4c08-a316-18b2f0efa618", + organizationId: "4748ad12-212e-4bc8-82b7-a75f6709d033", + folderId: "b4dac811-e44a-495a-9334-9e53b7aaf54c", collectionIds: [], key: "EncryptedString", name: "EncryptedString", @@ -1007,9 +1007,9 @@ describe("Cipher DTO", () => { it("should map from SDK Cipher", () => { jest.restoreAllMocks(); const sdkCipher: SdkCipher = { - id: "id", - organizationId: "orgId", - folderId: "folderId", + id: "id" as any, + organizationId: "orgId" as any, + folderId: "folderId" as any, collectionIds: [], key: "EncryptedString" as SdkEncString, name: "EncryptedString" as SdkEncString, diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index f884dc32cce..7364930e29c 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -2,10 +2,10 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { uuidToString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Cipher as SdkCipher } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; +import { asUuid, uuidAsString } from "../../../platform/abstractions/sdk/sdk.service"; import { Decryptable } from "../../../platform/interfaces/decryptable.interface"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; @@ -344,10 +344,10 @@ export class Cipher extends Domain implements Decryptable { */ toSdkCipher(): SdkCipher { const sdkCipher: SdkCipher = { - id: this.id, - organizationId: this.organizationId ?? undefined, - folderId: this.folderId ?? undefined, - collectionIds: this.collectionIds ?? [], + id: asUuid(this.id), + organizationId: this.organizationId ? asUuid(this.organizationId) : undefined, + folderId: this.folderId ? asUuid(this.folderId) : undefined, + collectionIds: this.collectionIds ? this.collectionIds.map(asUuid) : ([] as any), key: this.key?.toSdk(), name: this.name.toSdk(), notes: this.notes?.toSdk(), @@ -412,12 +412,12 @@ export class Cipher extends Domain implements Decryptable { const cipher = new Cipher(); - cipher.id = sdkCipher.id ? uuidToString(sdkCipher.id) : undefined; + cipher.id = sdkCipher.id ? uuidAsString(sdkCipher.id) : undefined; cipher.organizationId = sdkCipher.organizationId - ? uuidToString(sdkCipher.organizationId) + ? uuidAsString(sdkCipher.organizationId) : undefined; - cipher.folderId = sdkCipher.folderId ? uuidToString(sdkCipher.folderId) : undefined; - cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidToString) : []; + cipher.folderId = sdkCipher.folderId ? uuidAsString(sdkCipher.folderId) : undefined; + cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidAsString) : []; cipher.key = EncString.fromJSON(sdkCipher.key); cipher.name = EncString.fromJSON(sdkCipher.name); cipher.notes = EncString.fromJSON(sdkCipher.notes); diff --git a/libs/common/src/vault/models/view/cipher.view.spec.ts b/libs/common/src/vault/models/view/cipher.view.spec.ts index 46cea06979f..5dbdfdbbef2 100644 --- a/libs/common/src/vault/models/view/cipher.view.spec.ts +++ b/libs/common/src/vault/models/view/cipher.view.spec.ts @@ -14,6 +14,7 @@ import { } from "@bitwarden/sdk-internal"; import { mockFromJson, mockFromSdk } from "../../../../spec"; +import { asUuid } from "../../../platform/abstractions/sdk/sdk.service"; import { CipherRepromptType } from "../../enums"; import { CipherType } from "../../enums/cipher-type"; @@ -123,10 +124,10 @@ describe("CipherView", () => { jest.spyOn(FieldView, "fromSdkFieldView").mockImplementation(mockFromSdk); sdkCipherView = { - id: "id", - organizationId: "orgId", - folderId: "folderId", - collectionIds: ["collectionId"], + id: "id" as any, + organizationId: "orgId" as any, + folderId: "folderId" as any, + collectionIds: ["collectionId" as any], key: undefined, name: "name", notes: undefined, @@ -260,11 +261,11 @@ describe("CipherView", () => { const sdkCipherView = cipherView.toSdkCipherView(); expect(sdkCipherView).toMatchObject({ - id: "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602", - organizationId: "000f2a6e-da5e-4726-87ed-1c5c77322c3c", - folderId: "41b22db4-8e2a-4ed2-b568-f1186c72922f", - collectionIds: ["b0473506-3c3c-4260-a734-dfaaf833ab6f"], - key: "some-key", + id: asUuid("0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602"), + organizationId: asUuid("000f2a6e-da5e-4726-87ed-1c5c77322c3c"), + folderId: asUuid("41b22db4-8e2a-4ed2-b568-f1186c72922f"), + collectionIds: [asUuid("b0473506-3c3c-4260-a734-dfaaf833ab6f")], + key: "some-key" as any, name: "name", notes: "notes", type: SdkCipherType.Login, diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index c91d6e21ca2..015d2585850 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { uuidToString, asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; +import { asUuid, uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; import { View } from "../../../models/view/view"; @@ -256,9 +256,9 @@ export class CipherView implements View, InitializerMetadata { } const cipherView = new CipherView(); - cipherView.id = uuidToString(obj.id) ?? null; - cipherView.organizationId = uuidToString(obj.organizationId) ?? null; - cipherView.folderId = uuidToString(obj.folderId) ?? null; + cipherView.id = uuidAsString(obj.id) ?? null; + cipherView.organizationId = uuidAsString(obj.organizationId) ?? null; + cipherView.folderId = uuidAsString(obj.folderId) ?? null; cipherView.name = obj.name; cipherView.notes = obj.notes ?? null; cipherView.type = obj.type; @@ -273,7 +273,7 @@ export class CipherView implements View, InitializerMetadata { cipherView.fields = obj.fields?.map((f) => FieldView.fromSdkFieldView(f)) ?? []; cipherView.passwordHistory = obj.passwordHistory?.map((ph) => PasswordHistoryView.fromSdkPasswordHistoryView(ph)) ?? []; - cipherView.collectionIds = obj.collectionIds?.map((i) => uuidToString(i)) ?? []; + cipherView.collectionIds = obj.collectionIds?.map((i) => uuidAsString(i)) ?? []; cipherView.revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); cipherView.creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); cipherView.deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate); @@ -325,7 +325,7 @@ export class CipherView implements View, InitializerMetadata { attachments: this.attachments?.map((a) => a.toSdkAttachmentView()), fields: this.fields?.map((f) => f.toSdkFieldView()), passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistoryView()), - collectionIds: this.collectionIds?.map((i) => i) ?? [], + collectionIds: this.collectionIds?.map((i) => asUuid(i)) ?? [], // Revision and creation dates are non-nullable in SDKCipherView revisionDate: (this.revisionDate ?? new Date()).toISOString(), creationDate: (this.creationDate ?? new Date()).toISOString(), diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 06177629de5..7f7e2c3f531 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -8,6 +8,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { getByIds } from "@bitwarden/common/platform/misc"; import { getUserId } from "../../auth/services/account.service"; +import { uuidAsString } from "../../platform/abstractions/sdk/sdk.service"; import { CipherLike } from "../types/cipher-like"; /** @@ -140,7 +141,7 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer } return this.collectionService.decryptedCollections$(userId).pipe( - getByIds(cipher.collectionIds), + getByIds(cipher.collectionIds.map(uuidAsString)), map((allCollections) => allCollections.some((collection) => collection.manage)), ); }), diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8429c97be2e..a3915d6ce3d 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -31,6 +31,7 @@ import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; +import { uuidAsString } from "../../platform/abstractions/sdk/sdk.service"; import { Utils } from "../../platform/misc/utils"; import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; @@ -2035,7 +2036,7 @@ export class CipherService implements CipherServiceAbstraction { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - const cipher = await this.get(c.id!, activeUserId); + const cipher = await this.get(uuidAsString(c.id!), activeUserId); return this.decrypt(cipher, activeUserId); } diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts index 16e39421490..12e5b0b4626 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.spec.ts @@ -124,15 +124,15 @@ describe("DefaultCipherEncryptionService", () => { cipherViewObj = new CipherView(cipherObj); jest.spyOn(cipherObj, "toSdkCipher").mockImplementation(() => { - return { id: cipherData.id } as SdkCipher; + return { id: cipherData.id as any } as SdkCipher; }); jest.spyOn(cipherViewObj, "toSdkCipherView").mockImplementation(() => { - return { id: cipherData.id } as SdkCipherView; + return { id: cipherData.id as any } as SdkCipherView; }); sdkCipherView = { - id: cipherId as string, + id: cipherId as any, type: SdkCipherType.Login, name: "test-name", login: { @@ -334,7 +334,7 @@ describe("DefaultCipherEncryptionService", () => { .vault() .ciphers() .set_fido2_credentials.mockReturnValue({ - id: cipherId as string, + id: cipherId as any, login: { fido2Credentials: [mockSdkCredentialView], }, @@ -519,8 +519,8 @@ describe("DefaultCipherEncryptionService", () => { const ciphers = [new Cipher(cipherData), new Cipher(cipherData)]; const expectedListViews = [ - { id: "list1", name: "List 1" } as CipherListView, - { id: "list2", name: "List 2" } as CipherListView, + { id: "list1" as any, name: "List 1" } as CipherListView, + { id: "list2" as any, name: "List 2" } as CipherListView, ]; mockSdkClient.vault().ciphers().decrypt_list.mockReturnValue(expectedListViews); @@ -554,7 +554,7 @@ describe("DefaultCipherEncryptionService", () => { const encryptedContent = new Uint8Array([1, 2, 3, 4]); const expectedDecryptedContent = new Uint8Array([5, 6, 7, 8]); - jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" } as SdkCipher); + jest.spyOn(cipher, "toSdkCipher").mockReturnValue({ id: "id" as any } as SdkCipher); jest .spyOn(attachment, "toSdkAttachmentView") .mockReturnValue({ id: "a1" } as SdkAttachmentView); diff --git a/libs/common/src/vault/services/default-cipher-encryption.service.ts b/libs/common/src/vault/services/default-cipher-encryption.service.ts index 2cef4ca1ca1..b7026fd4cfc 100644 --- a/libs/common/src/vault/services/default-cipher-encryption.service.ts +++ b/libs/common/src/vault/services/default-cipher-encryption.service.ts @@ -9,7 +9,7 @@ import { } from "@bitwarden/sdk-internal"; import { LogService } from "../../platform/abstractions/log.service"; -import { SdkService, asUuid } from "../../platform/abstractions/sdk/sdk.service"; +import { SdkService, asUuid, uuidAsString } from "../../platform/abstractions/sdk/sdk.service"; import { UserId, OrganizationId } from "../../types/guid"; import { CipherEncryptionService } from "../abstractions/cipher-encryption.service"; import { CipherType } from "../enums"; @@ -39,7 +39,7 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { return { cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, - encryptedFor: asUuid(encryptionContext.encryptedFor), + encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId, }; }), catchError((error: unknown) => { @@ -74,7 +74,7 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { return { cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, - encryptedFor: asUuid(encryptionContext.encryptedFor), + encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId, }; }), catchError((error: unknown) => { @@ -107,7 +107,7 @@ export class DefaultCipherEncryptionService implements CipherEncryptionService { return { cipher: Cipher.fromSdkCipher(encryptionContext.cipher)!, - encryptedFor: asUuid(encryptionContext.encryptedFor), + encryptedFor: uuidAsString(encryptionContext.encryptedFor) as UserId, }; }), catchError((error: unknown) => { diff --git a/libs/common/src/vault/services/restricted-item-types.service.ts b/libs/common/src/vault/services/restricted-item-types.service.ts index 0e4c9b87716..206bc9e2cb7 100644 --- a/libs/common/src/vault/services/restricted-item-types.service.ts +++ b/libs/common/src/vault/services/restricted-item-types.service.ts @@ -10,6 +10,7 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { uuidAsString } from "../../platform/abstractions/sdk/sdk.service"; import { CipherLike } from "../types/cipher-like"; import { CipherViewLikeUtils } from "../utils/cipher-view-like-utils"; @@ -108,7 +109,7 @@ export class RestrictedItemTypesService { // If cipher belongs to an organization if (cipher.organizationId) { // Check if this organization allows viewing this cipher type - return !restriction.allowViewOrgIds.includes(cipher.organizationId); + return !restriction.allowViewOrgIds.includes(uuidAsString(cipher.organizationId)); } // Cipher is restricted by at least one organization, restrict it diff --git a/libs/common/src/vault/services/search.service.ts b/libs/common/src/vault/services/search.service.ts index 041f978f4ea..cbd89cf1ab1 100644 --- a/libs/common/src/vault/services/search.service.ts +++ b/libs/common/src/vault/services/search.service.ts @@ -9,6 +9,7 @@ import { perUserCache$ } from "@bitwarden/common/vault/utils/observable-utilitie import { UriMatchStrategy } from "../../models/domain/domain-service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { LogService } from "../../platform/abstractions/log.service"; +import { uuidAsString } from "../../platform/abstractions/sdk/sdk.service"; import { SingleUserState, StateProvider, @@ -261,7 +262,7 @@ export class SearchService implements SearchServiceAbstraction { } const ciphersMap = new Map(); - ciphers.forEach((c) => ciphersMap.set(c.id, c)); + ciphers.forEach((c) => ciphersMap.set(uuidAsString(c.id), c)); let searchResults: lunr.Index.Result[] = null; const isQueryString = query != null && query.length > 1 && query.indexOf(">") === 0; @@ -304,7 +305,7 @@ export class SearchService implements SearchServiceAbstraction { if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) { return true; } - if (query.length >= 8 && c.id.startsWith(query)) { + if (query.length >= 8 && uuidAsString(c.id).startsWith(query)) { return true; } const subtitle = CipherViewLikeUtils.subtitle(c); diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts index f302340ef9e..0df44e67f1b 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.spec.ts @@ -175,13 +175,13 @@ describe("CipherViewLikeUtils", () => { }); it("returns false when the cipher is assigned to an organization and cannot be edited", () => { - cipherListView.organizationId = "org-id"; + cipherListView.organizationId = "org-id" as any; expect(CipherViewLikeUtils.canAssignToCollections(cipherListView)).toBe(false); }); it("returns true when the cipher is assigned to an organization and can be edited", () => { - cipherListView.organizationId = "org-id"; + cipherListView.organizationId = "org-id" as any; cipherListView.edit = true; cipherListView.viewPassword = true; diff --git a/libs/vault/src/components/copy-cipher-field.directive.ts b/libs/vault/src/components/copy-cipher-field.directive.ts index 59ad8bf38e8..9725adae5e2 100644 --- a/libs/vault/src/components/copy-cipher-field.directive.ts +++ b/libs/vault/src/components/copy-cipher-field.directive.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -100,7 +101,10 @@ export class CopyCipherFieldDirective implements OnChanges { const activeAccountId = await firstValueFrom( this.accountService.activeAccount$.pipe(getUserId), ); - const encryptedCipher = await this.cipherService.get(this.cipher.id!, activeAccountId); + const encryptedCipher = await this.cipherService.get( + uuidAsString(this.cipher.id!), + activeAccountId, + ); _cipher = await this.cipherService.decrypt(encryptedCipher, activeAccountId); } else { _cipher = this.cipher; diff --git a/libs/vault/src/services/copy-cipher-field.service.ts b/libs/vault/src/services/copy-cipher-field.service.ts index 606614f2143..d0086ab5ac7 100644 --- a/libs/vault/src/services/copy-cipher-field.service.ts +++ b/libs/vault/src/services/copy-cipher-field.service.ts @@ -7,6 +7,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherRepromptType } from "@bitwarden/common/vault/enums"; import { @@ -144,9 +145,9 @@ export class CopyCipherFieldService { if (action.event !== undefined) { await this.eventCollectionService.collect( action.event, - cipher.id, + uuidAsString(cipher.id), false, - cipher.organizationId, + uuidAsString(cipher.organizationId), ); } diff --git a/package-lock.json b/package-lock.json index 9e65708076b..7c99ed85173 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.242", + "@bitwarden/sdk-internal": "0.2.0-main.266", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", @@ -4682,9 +4682,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.242", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.242.tgz", - "integrity": "sha512-LFPNAAq9ORVGdvcB3PBhlM3GQZUMf3MhIuYbZxmhAG5SVlvem+sbaolgK3Fnf/8ajVx1IDMNEhfgQkA4mU9uAg==", + "version": "0.2.0-main.266", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.266.tgz", + "integrity": "sha512-2Axa1D9AEkax2ssqahZYHVkk2RdguzLV2bJ6j99AZhh4qjGIYtDvmc5gDh7zhuw7Ig7H3mNpKwCZ/eJgadyH6g==", "license": "GPL-3.0", "dependencies": { "type-fest": "^4.41.0" diff --git a/package.json b/package.json index d670ca2d002..511424bc425 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "@angular/platform-browser": "19.2.14", "@angular/platform-browser-dynamic": "19.2.14", "@angular/router": "19.2.14", - "@bitwarden/sdk-internal": "0.2.0-main.242", + "@bitwarden/sdk-internal": "0.2.0-main.266", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "4.0.0", From ec950853bc4f60f050ab0c63c48b8d862dfe6cda Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:25:31 -0700 Subject: [PATCH 03/18] fix(2fa-recovery-code-error): [Auth/PM-19885] Better error handling when 2FA recovery code is invalid (#16145) Implements better error handling when a user enters an invalid 2FA recovery code. Upon entering an invalid code: - Keep the user on the `/recover-2fa` page (This also makes it so the incorrect code remains in the form field so the user can see what they entered, if they mistyped the code, etc.) - Show an inline error: "Invalid recovery code" --- .../auth/recover-two-factor.component.html | 1 - .../auth/recover-two-factor.component.spec.ts | 74 +++++++++++++++---- .../app/auth/recover-two-factor.component.ts | 33 +++++++-- apps/web/src/locales/en/messages.json | 3 + 4 files changed, 90 insertions(+), 21 deletions(-) diff --git a/apps/web/src/app/auth/recover-two-factor.component.html b/apps/web/src/app/auth/recover-two-factor.component.html index dee3bec1520..da2501efe26 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.html +++ b/apps/web/src/app/auth/recover-two-factor.component.html @@ -28,7 +28,6 @@ {{ "recoveryCodeTitle" | i18n }} -
+
+ +
+
diff --git a/libs/vault/src/components/carousel/carousel.component.spec.ts b/libs/vault/src/components/carousel/carousel.component.spec.ts index 1409aea0cb2..ebb38576813 100644 --- a/libs/vault/src/components/carousel/carousel.component.spec.ts +++ b/libs/vault/src/components/carousel/carousel.component.spec.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; import { VaultCarouselComponent } from "./carousel.component"; @@ -33,6 +35,7 @@ describe("VaultCarouselComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], }).compileComponents(); }); @@ -48,7 +51,7 @@ describe("VaultCarouselComponent", () => { it("shows the active slides content", () => { // Set the second slide as active - fixture.debugElement.queryAll(By.css("button"))[1].nativeElement.click(); + fixture.debugElement.queryAll(By.css("button"))[2].nativeElement.click(); fixture.detectChanges(); const heading = fixture.debugElement.query(By.css("h1")).nativeElement; @@ -63,10 +66,37 @@ describe("VaultCarouselComponent", () => { it('emits "slideChange" event when slide changes', () => { jest.spyOn(component.slideChange, "emit"); - const thirdSlideButton = fixture.debugElement.queryAll(By.css("button"))[2]; + const thirdSlideButton = fixture.debugElement.queryAll(By.css("button"))[3]; thirdSlideButton.nativeElement.click(); expect(component.slideChange.emit).toHaveBeenCalledWith(2); }); + + it('advances to the next slide when the "next" button is pressed', () => { + const middleSlideButton = fixture.debugElement.queryAll(By.css("button"))[2]; + const nextButton = fixture.debugElement.queryAll(By.css("button"))[4]; + + middleSlideButton.nativeElement.click(); + + jest.spyOn(component.slideChange, "emit"); + + nextButton.nativeElement.click(); + + expect(component.slideChange.emit).toHaveBeenCalledWith(2); + }); + + it('advances to the previous slide when the "back" button is pressed', async () => { + const middleSlideButton = fixture.debugElement.queryAll(By.css("button"))[2]; + const backButton = fixture.debugElement.queryAll(By.css("button"))[0]; + + middleSlideButton.nativeElement.click(); + await new Promise((r) => setTimeout(r, 100)); // Give time for the DOM to update. + + jest.spyOn(component.slideChange, "emit"); + + backButton.nativeElement.click(); + + expect(component.slideChange.emit).toHaveBeenCalledWith(0); + }); }); diff --git a/libs/vault/src/components/carousel/carousel.component.ts b/libs/vault/src/components/carousel/carousel.component.ts index f2d211697df..fdebbebc33b 100644 --- a/libs/vault/src/components/carousel/carousel.component.ts +++ b/libs/vault/src/components/carousel/carousel.component.ts @@ -20,7 +20,9 @@ import { import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { take } from "rxjs"; -import { ButtonModule } from "@bitwarden/components"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ButtonModule, IconButtonModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; import { VaultCarouselButtonComponent } from "./carousel-button/carousel-button.component"; import { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component"; @@ -32,9 +34,12 @@ import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.com imports: [ CdkPortalOutlet, CommonModule, + JslibModule, + IconButtonModule, ButtonModule, VaultCarouselContentComponent, VaultCarouselButtonComponent, + I18nPipe, ], }) export class VaultCarouselComponent implements AfterViewInit { @@ -97,6 +102,18 @@ export class VaultCarouselComponent implements AfterViewInit { this.slideChange.emit(index); } + protected nextSlide() { + if (this.selectedIndex < this.slides.length - 1) { + this.selectSlide(this.selectedIndex + 1); + } + } + + protected prevSlide() { + if (this.selectedIndex > 0) { + this.selectSlide(this.selectedIndex - 1); + } + } + async ngAfterViewInit() { this.keyManager = new FocusKeyManager(this.carouselButtons) .withHorizontalOrientation("ltr") diff --git a/libs/vault/src/components/carousel/carousel.stories.ts b/libs/vault/src/components/carousel/carousel.stories.ts index 521a561a19f..1e393779a6a 100644 --- a/libs/vault/src/components/carousel/carousel.stories.ts +++ b/libs/vault/src/components/carousel/carousel.stories.ts @@ -1,5 +1,6 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ButtonComponent, TypographyModule } from "@bitwarden/components"; import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; @@ -11,6 +12,7 @@ export default { decorators: [ moduleMetadata({ imports: [VaultCarouselSlideComponent, TypographyModule, ButtonComponent], + providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], }), ], } as Meta; From a4fca832f31f626cf06ce7e63268e2ce00c3d5d8 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Tue, 2 Sep 2025 15:15:38 -0400 Subject: [PATCH 17/18] [PM-22312] Resolve TS 5.8 errors (#16108) * refactor: remove ts-strict-ignore and update * refactor: remove ts-strict-ignore and update * refactor: simplify if statement * refactor: remove ts-strict-ignore and update * refactor: add nullable union for interfaces --- .../trial-billing-step.component.ts | 2 +- ...organization-payment-method.component.html | 2 +- .../organization-payment-method.component.ts | 53 ++++++++------- .../adjust-payment-dialog.component.ts | 2 +- .../shared/payment-method.component.ts | 64 +++++++++++-------- .../complete-trial-initiation.component.html | 6 +- .../complete-trial-initiation.component.ts | 51 +++++++++------ 7 files changed, 105 insertions(+), 75 deletions(-) diff --git a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts index 7e25a422477..20e69cf3bfd 100644 --- a/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts +++ b/apps/web/src/app/billing/accounts/trial-initiation/trial-billing-step.component.ts @@ -42,7 +42,7 @@ export type TrialOrganizationType = Exclude - {{ freeTrialData.message }} + {{ freeTrialData?.message }} a?.id)), ); + + if (!userId) { + throw new Error("User ID is not found"); + } + const organizationPromise = await firstValueFrom( this.organizationService .organizations$(userId) @@ -173,15 +176,20 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { organizationSubscriptionPromise, organizationPromise, ]); + + if (!this.organization) { + throw new Error("Organization is not found"); + } + if (!this.paymentSource) { + throw new Error("Payment source is not found"); + } + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( this.organization, this.organizationSubscriptionResponse, - paymentSource, + this.paymentSource, ); } - // TODO: Eslint upgrade. Please resolve this since the ?? does nothing - // eslint-disable-next-line no-constant-binary-expression - this.isUnpaid = this.subscriptionStatus === "unpaid" ?? false; // If the flag `launchPaymentModalAutomatically` is set to true, // we schedule a timeout (delay of 800ms) to automatically launch the payment modal. // This delay ensures that any prior UI/rendering operations complete before triggering the modal. @@ -219,14 +227,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { const dialogRef = TrialPaymentDialogComponent.open(this.dialogService, { data: { organizationId: this.organizationId, - subscription: this.organizationSubscriptionResponse, - productTierType: this.organization?.productTierType, + subscription: this.organizationSubscriptionResponse!, + productTierType: this.organization!.productTierType, }, }); const result = await lastValueFrom(dialogRef.closed); if (result === TRIAL_PAYMENT_METHOD_DIALOG_RESULT_TYPE.SUBMITTED) { this.location.replaceState(this.location.path(), "", {}); - if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + if (this.launchPaymentModalAutomatically && !this.organization?.enabled) { await this.syncService.fullSync(true); } this.launchPaymentModalAutomatically = false; @@ -238,13 +246,14 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("verifiedBankAccount"), }); }; protected get accountCreditHeaderText(): string { - const key = this.accountCredit <= 0 ? "accountBalance" : "accountCredit"; + const hasAccountCredit = this.accountCredit && this.accountCredit > 0; + const key = hasAccountCredit ? "accountCredit" : "accountBalance"; return this.i18nService.t(key); } @@ -279,7 +288,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy { if (!hasBillingAddress) { this.toastService.showToast({ variant: "error", - title: null, + title: "", message: this.i18nService.t("billingAddressRequiredToAddCredit"), }); return false; diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts index 94929c58656..9944085488f 100644 --- a/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog/adjust-payment-dialog.component.ts @@ -24,7 +24,7 @@ import { import { PaymentComponent } from "../payment/payment.component"; export interface AdjustPaymentDialogParams { - initialPaymentMethod?: PaymentMethodType; + initialPaymentMethod?: PaymentMethodType | null; organizationId?: string; productTier?: ProductTierType; providerId?: string; diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index 0e116b4f39a..91d5925669a 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; @@ -42,21 +40,21 @@ import { export class PaymentMethodComponent implements OnInit, OnDestroy { loading = false; firstLoaded = false; - billing: BillingPaymentResponse; - org: OrganizationSubscriptionResponse; - sub: SubscriptionResponse; + billing?: BillingPaymentResponse; + org?: OrganizationSubscriptionResponse; + sub?: SubscriptionResponse; paymentMethodType = PaymentMethodType; - organizationId: string; + organizationId?: string; isUnpaid = false; - organization: Organization; + organization?: Organization; verifyBankForm = this.formBuilder.group({ - amount1: new FormControl(null, [ + amount1: new FormControl(0, [ Validators.required, Validators.max(99), Validators.min(0), ]), - amount2: new FormControl(null, [ + amount2: new FormControl(0, [ Validators.required, Validators.max(99), Validators.min(0), @@ -64,7 +62,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { }); launchPaymentModalAutomatically = false; - protected freeTrialData: FreeTrial; + protected freeTrialData?: FreeTrial; constructor( protected apiService: ApiService, @@ -84,7 +82,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { private configService: ConfigService, ) { const state = this.router.getCurrentNavigation()?.extras?.state; - // incase the above state is undefined or null we use redundantState + // In case the above state is undefined or null, we use redundantState const redundantState: any = location.getState(); if (state && Object.prototype.hasOwnProperty.call(state, "launchPaymentModalAutomatically")) { this.launchPaymentModalAutomatically = state.launchPaymentModalAutomatically; @@ -129,17 +127,23 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { } this.loading = true; if (this.forOrganization) { - const billingPromise = this.organizationApiService.getBilling(this.organizationId); + const billingPromise = this.organizationApiService.getBilling(this.organizationId!); const organizationSubscriptionPromise = this.organizationApiService.getSubscription( - this.organizationId, + this.organizationId!, ); + const userId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + + if (!userId) { + throw new Error("User ID is not found"); + } + const organizationPromise = await firstValueFrom( this.organizationService .organizations$(userId) - .pipe(getOrganizationById(this.organizationId)), + .pipe(getOrganizationById(this.organizationId!)), ); [this.billing, this.org, this.organization] = await Promise.all([ @@ -171,14 +175,16 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { }; addCredit = async () => { - const dialogRef = openAddCreditDialog(this.dialogService, { - data: { - organizationId: this.organizationId, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AddCreditDialogResult.Added) { - await this.load(); + if (this.forOrganization) { + const dialogRef = openAddCreditDialog(this.dialogService, { + data: { + organizationId: this.organizationId!, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AddCreditDialogResult.Added) { + await this.load(); + } } }; @@ -194,7 +200,7 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { if (result === AdjustPaymentDialogResultType.Submitted) { this.location.replaceState(this.location.path(), "", {}); - if (this.launchPaymentModalAutomatically && !this.organization.enabled) { + if (this.launchPaymentModalAutomatically && !this.organization?.enabled) { await this.syncService.fullSync(true); } this.launchPaymentModalAutomatically = false; @@ -208,18 +214,22 @@ export class PaymentMethodComponent implements OnInit, OnDestroy { } const request = new VerifyBankRequest(); - request.amount1 = this.verifyBankForm.value.amount1; - request.amount2 = this.verifyBankForm.value.amount2; - await this.organizationApiService.verifyBank(this.organizationId, request); + request.amount1 = this.verifyBankForm.value.amount1!; + request.amount2 = this.verifyBankForm.value.amount2!; + await this.organizationApiService.verifyBank(this.organizationId!, request); this.toastService.showToast({ variant: "success", - title: null, + title: "", message: this.i18nService.t("verifiedBankAccount"), }); await this.load(); }; determineOrgsWithUpcomingPaymentIssues() { + if (!this.organization || !this.org || !this.billing) { + throw new Error("Organization, organization subscription, or billing is not defined"); + } + this.freeTrialData = this.trialFlowService.checkForOrgsWithUpcomingPaymentIssues( this.organization, this.org, diff --git a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html index e74997cb9f5..c1a33a4c8df 100644 --- a/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html +++ b/apps/web/src/app/billing/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html @@ -37,7 +37,7 @@ bitButton buttonType="primary" [disabled]="orgInfoFormGroup.controls.name.invalid" - [loading]="loading && (trialPaymentOptional$ | async)" + [loading]="loading && (trialPaymentOptional$ | async)!" (click)="orgNameEntrySubmit()" > {{ @@ -55,8 +55,8 @@ { + .catch((e: unknown): null => { this.validationService.showError(e); this.submitting = false; return null; From 5967cf05394c8ef65c4a7c4cd6039e6d8a32b940 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:09:20 -0500 Subject: [PATCH 18/18] [PM-14571] At Risk Passwords - Badge Update (#15983) * add exclamation badge for at risk passwords on tab * add berry icon for the badge when pending tasks are present * remove integration wtih autofill for pending task badge * add ability to override Never match strategy - This is helpful for non-autofill purposes but cipher matching is still needed. This will default to the domain. * add at-risk-cipher badge updater service * Revert "add exclamation badge for at risk passwords on tab" This reverts commit a9643c03d5ff812a88d554b4a4bcb13f0d5444f0. * remove nullish-coalescing * ensure that all user related observables use the same user.id --------- Co-authored-by: Shane Melton --- .../browser/src/background/main.background.ts | 11 ++ apps/browser/src/images/berry19.png | Bin 0 -> 1702 bytes apps/browser/src/images/berry38.png | Bin 0 -> 1244 bytes apps/browser/src/platform/badge/icon.ts | 4 + ...-risk-cipher-badge-updater.service.spec.ts | 84 +++++++++ .../at-risk-cipher-badge-updater.service.ts | 163 ++++++++++++++++++ .../src/vault/abstractions/cipher.service.ts | 4 + .../vault/models/view/login-uri-view.spec.ts | 27 +++ .../src/vault/models/view/login-uri.view.ts | 8 + .../src/vault/models/view/login.view.ts | 6 +- .../src/vault/services/cipher.service.ts | 11 +- .../src/vault/utils/cipher-view-like-utils.ts | 10 +- 12 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 apps/browser/src/images/berry19.png create mode 100644 apps/browser/src/images/berry38.png create mode 100644 apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.spec.ts create mode 100644 apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index df29502edeb..75481dde8cf 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -302,6 +302,7 @@ import { OffscreenStorageService } from "../platform/storage/offscreen-storage.s import { SyncServiceListener } from "../platform/sync/sync-service.listener"; import { BrowserSystemNotificationService } from "../platform/system-notifications/browser-system-notification.service"; import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; +import { AtRiskCipherBadgeUpdaterService } from "../vault/services/at-risk-cipher-badge-updater.service"; import CommandsBackground from "./commands.background"; import IdleBackground from "./idle.background"; @@ -433,6 +434,7 @@ export default class MainBackground { badgeService: BadgeService; authStatusBadgeUpdaterService: AuthStatusBadgeUpdaterService; autofillBadgeUpdaterService: AutofillBadgeUpdaterService; + atRiskCipherUpdaterService: AtRiskCipherBadgeUpdaterService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -1838,6 +1840,14 @@ export default class MainBackground { this.logService, ); + this.atRiskCipherUpdaterService = new AtRiskCipherBadgeUpdaterService( + this.badgeService, + this.accountService, + this.cipherService, + this.logService, + this.taskService, + ); + this.tabsBackground = new TabsBackground( this, this.notificationBackground, @@ -1847,6 +1857,7 @@ export default class MainBackground { await this.overlayBackground.init(); await this.tabsBackground.init(); await this.autofillBadgeUpdaterService.init(); + await this.atRiskCipherUpdaterService.init(); } generatePassword = async (): Promise => { diff --git a/apps/browser/src/images/berry19.png b/apps/browser/src/images/berry19.png new file mode 100644 index 0000000000000000000000000000000000000000..51deb3b8d68de97ce8521d376b2823eaecd12824 GIT binary patch literal 1702 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3HQmUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lxRtf@J#ddWzYh$IT%lpi<;HsXMd|v6mX?>t*GR!Iveo=5iVsfgTA=EA;AtcoxS6lfPWu^iH6clVA| zx>w8XSLgoSaxLxF3?HMJAMbFkT&z^}F!kM@-|uD?zqi%r+Vb-D{cEorudZS)mCp_3 zxjyx6ebmHvYKs|5%@cRFtF6}%^>770$x;Qincd%%qGvf;DyTuzVOG?w^o5@-lCZhs!@`GSXQiR(1)tS*D? zcMi;1d8*>$O_!C9GHIgKx{s!B-dR(~XI^sc$p*Pz2HR$_YuD?Ul3KU~-um%quH1UL=|5_p-ww1rUeG%8%l%@voCVvQ4u72O`6f%&;X3=O z$=h-q1<%RN>7L;8v0w3IZKVDWmtj-N zt%Hw$NL1NHbeOI^$a3#s<@~3cCLX$-;~PA8nt^d_R+{tovb!Adt&>}4f3{oQ^2$W% zc|_HJb@q)~vbBFL-$br`7*cF`Ozz0uOD%!EkKNXm+zl!@-fp&Ga|44GultXn!{PZF zoAiI_EZp{Ed#e_6#j%g?pSsN6<{B7#Qf{NkmdKI;Vst08`R>5C8xG literal 0 HcmV?d00001 diff --git a/apps/browser/src/images/berry38.png b/apps/browser/src/images/berry38.png new file mode 100644 index 0000000000000000000000000000000000000000..44a670637010dc0a565f5d67efc796a85120dff2 GIT binary patch literal 1244 zcmV<21S9*2P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91CZGcV1ONa40RR91CIA2c0EF@&TL1tAMoC0LR9Fe^SZ{1oRTTfdx4Mem z6p^it$ow!Mgds+VZkS<&7>yWbB9yU2gBlYt86Wf$Ve-Xx>G5o2Djl1@R`JQnC0jx!x`_Laj{S}O;&>>XJdb3kNb2O)Ik;{0LTHI3M)sXX$bS~G zfsh1vmU`j!-G#cRny|WUDNfc0(DK$*NiO>lOjAszfUZx*O09q^|FWyr}8!^Pvhhj^NfP*I@`1V|kw*?97rrEbdX6mQS?<4ke(9)kaVwhF> z(04{+jJEZSQT#eAKaeehn+@BVT(YKu`+96irTs`Q_(6~Kp|2xik*gXuc8y@L0CIOW z86O<1DW?+p*ubv1yT%zY2EG_h;jvxAIPpvF*3O*RJGTPwba>&ZtCs3?p=V;O?$cP$ zf@T81a>)tNc4C?<^L|l-M*c?H|LI-J&MO zvrmR)^=^3FMs*}cjj(J9P9=?v1tU763^{eo#C_d+1#7lnz`IAoNT#ySRE~!Sj5fnQ zy0_+A{E__Ov;YV=4sc3EX`wbk { + let service: AtRiskCipherBadgeUpdaterService; + + let setState: jest.Mock; + let clearState: jest.Mock; + let warning: jest.Mock; + let getAllDecryptedForUrl: jest.Mock; + let getTab: jest.Mock; + let addListener: jest.Mock; + + const activeAccount$ = new BehaviorSubject({ id: "test-account-id" }); + const cipherViews$ = new BehaviorSubject([]); + const pendingTasks$ = new BehaviorSubject([]); + const userId = "test-user-id" as UserId; + + beforeEach(async () => { + setState = jest.fn().mockResolvedValue(undefined); + clearState = jest.fn().mockResolvedValue(undefined); + warning = jest.fn(); + getAllDecryptedForUrl = jest.fn().mockResolvedValue([]); + getTab = jest.fn(); + addListener = jest.fn(); + + jest.spyOn(BrowserApi, "addListener").mockImplementation(addListener); + jest.spyOn(BrowserApi, "getTab").mockImplementation(getTab); + + service = new AtRiskCipherBadgeUpdaterService( + { setState, clearState } as unknown as BadgeService, + { activeAccount$ } as unknown as AccountService, + { cipherViews$, getAllDecryptedForUrl } as unknown as CipherService, + { warning } as unknown as LogService, + { pendingTasks$ } as unknown as TaskService, + ); + + await service.init(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("clears the tab state when there are no ciphers and no pending tasks", async () => { + const tab = { id: 1 } as chrome.tabs.Tab; + + await service["setTabState"](tab, userId, []); + + expect(clearState).toHaveBeenCalledWith("at-risk-cipher-badge-1"); + }); + + it("sets state when there are pending tasks for the tab", async () => { + const tab = { id: 3, url: "https://bitwarden.com" } as chrome.tabs.Tab; + const pendingTasks: SecurityTask[] = [{ id: "task1", cipherId: "cipher1" } as SecurityTask]; + getAllDecryptedForUrl.mockResolvedValueOnce([{ id: "cipher1" }]); + + await service["setTabState"](tab, userId, pendingTasks); + + expect(setState).toHaveBeenCalledWith( + "at-risk-cipher-badge-3", + BadgeStatePriority.High, + { + icon: BadgeIcon.Berry, + text: Unset, + backgroundColor: Unset, + }, + 3, + ); + }); +}); diff --git a/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts b/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts new file mode 100644 index 00000000000..47364958ad8 --- /dev/null +++ b/apps/browser/src/vault/services/at-risk-cipher-badge-updater.service.ts @@ -0,0 +1,163 @@ +import { combineLatest, map, mergeMap, of, Subject, switchMap } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SecurityTask, SecurityTaskType, TaskService } from "@bitwarden/common/vault/tasks"; +import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities"; + +import { BadgeService } from "../../platform/badge/badge.service"; +import { BadgeIcon } from "../../platform/badge/icon"; +import { BadgeStatePriority } from "../../platform/badge/priority"; +import { Unset } from "../../platform/badge/state"; +import { BrowserApi } from "../../platform/browser/browser-api"; + +const StateName = (tabId: number) => `at-risk-cipher-badge-${tabId}`; + +export class AtRiskCipherBadgeUpdaterService { + private tabReplaced$ = new Subject<{ addedTab: chrome.tabs.Tab; removedTabId: number }>(); + private tabUpdated$ = new Subject(); + private tabRemoved$ = new Subject(); + private tabActivated$ = new Subject(); + + private activeUserData$ = this.accountService.activeAccount$.pipe( + filterOutNullish(), + switchMap((user) => + combineLatest([ + of(user.id), + this.taskService + .pendingTasks$(user.id) + .pipe( + map((tasks) => tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential)), + ), + this.cipherService.cipherViews$(user.id).pipe(filterOutNullish()), + ]), + ), + ); + + constructor( + private badgeService: BadgeService, + private accountService: AccountService, + private cipherService: CipherService, + private logService: LogService, + private taskService: TaskService, + ) { + combineLatest({ + replaced: this.tabReplaced$, + activeUserData: this.activeUserData$, + }) + .pipe( + mergeMap(async ({ replaced, activeUserData: [userId, pendingTasks] }) => { + await this.clearTabState(replaced.removedTabId); + await this.setTabState(replaced.addedTab, userId, pendingTasks); + }), + ) + .subscribe(() => {}); + + combineLatest({ + tab: this.tabActivated$, + activeUserData: this.activeUserData$, + }) + .pipe( + mergeMap(async ({ tab, activeUserData: [userId, pendingTasks] }) => { + await this.setTabState(tab, userId, pendingTasks); + }), + ) + .subscribe(); + + combineLatest({ + tab: this.tabUpdated$, + activeUserData: this.activeUserData$, + }) + .pipe( + mergeMap(async ({ tab, activeUserData: [userId, pendingTasks] }) => { + await this.setTabState(tab, userId, pendingTasks); + }), + ) + .subscribe(); + + this.tabRemoved$ + .pipe( + mergeMap(async (tabId) => { + await this.clearTabState(tabId); + }), + ) + .subscribe(); + } + + init() { + BrowserApi.addListener(chrome.tabs.onReplaced, async (addedTabId, removedTabId) => { + const newTab = await BrowserApi.getTab(addedTabId); + if (!newTab) { + this.logService.warning( + `Tab replaced event received but new tab not found (id: ${addedTabId})`, + ); + return; + } + + this.tabReplaced$.next({ + removedTabId, + addedTab: newTab, + }); + }); + + BrowserApi.addListener(chrome.tabs.onUpdated, (_, changeInfo, tab) => { + if (changeInfo.url) { + this.tabUpdated$.next(tab); + } + }); + + BrowserApi.addListener(chrome.tabs.onActivated, async (activeInfo) => { + const tab = await BrowserApi.getTab(activeInfo.tabId); + if (!tab) { + this.logService.warning( + `Tab activated event received but tab not found (id: ${activeInfo.tabId})`, + ); + return; + } + + this.tabActivated$.next(tab); + }); + + BrowserApi.addListener(chrome.tabs.onRemoved, (tabId, _) => this.tabRemoved$.next(tabId)); + } + + /** Sets the pending task state for the tab */ + private async setTabState(tab: chrome.tabs.Tab, userId: UserId, pendingTasks: SecurityTask[]) { + if (!tab.id) { + this.logService.warning("Tab event received but tab id is undefined"); + return; + } + + const ciphers = tab.url + ? await this.cipherService.getAllDecryptedForUrl(tab.url, userId, [], undefined, true) + : []; + + const hasPendingTasksForTab = pendingTasks.some((task) => + ciphers.some((cipher) => cipher.id === task.cipherId && !cipher.isDeleted), + ); + + if (!hasPendingTasksForTab) { + await this.clearTabState(tab.id); + return; + } + + await this.badgeService.setState( + StateName(tab.id), + BadgeStatePriority.High, + { + icon: BadgeIcon.Berry, + // Unset text and background color to use default badge appearance + text: Unset, + backgroundColor: Unset, + }, + tab.id, + ); + } + + /** Clears the pending task state from a tab */ + private async clearTabState(tabId: number) { + await this.badgeService.clearState(StateName(tabId)); + } +} diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 2f4fcf0ef51..7eb2d4b0656 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -65,12 +65,16 @@ export abstract class CipherService implements UserKeyRotationDataProvider; abstract filterCiphersForUrl( ciphers: C[], url: string, includeOtherTypes?: CipherType[], defaultMatch?: UriMatchStrategySetting, + /** When true, will override the match strategy for the cipher if it is Never. */ + overrideNeverMatchStrategy?: true, ): Promise; abstract getAllFromApiForOrganization(organizationId: string): Promise; /** diff --git a/libs/common/src/vault/models/view/login-uri-view.spec.ts b/libs/common/src/vault/models/view/login-uri-view.spec.ts index 155d3d59f7c..aae9438df2e 100644 --- a/libs/common/src/vault/models/view/login-uri-view.spec.ts +++ b/libs/common/src/vault/models/view/login-uri-view.spec.ts @@ -111,6 +111,33 @@ describe("LoginUriView", () => { expect(actual).toBe(false); }); + + it("overrides Never match strategy with Domain when parameter is set", () => { + const loginUri = new LoginUriView(); + loginUri.uri = "https://example.org"; + loginUri.match = UriMatchStrategy.Never; + + expect(loginUri.matchesUri("https://example.org", new Set(), undefined, true)).toBe(true); + expect(loginUri.matchesUri("https://example.org", new Set(), undefined)).toBe(false); + }); + + it("overrides Never match strategy when passed in as default strategy", () => { + const loginUriNoMatch = new LoginUriView(); + loginUriNoMatch.uri = "https://example.org"; + + expect( + loginUriNoMatch.matchesUri( + "https://example.org", + new Set(), + UriMatchStrategy.Never, + true, + ), + ).toBe(true); + + expect( + loginUriNoMatch.matchesUri("https://example.org", new Set(), UriMatchStrategy.Never), + ).toBe(false); + }); }); describe("using host matching", () => { diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index 38cd517e542..49ac9c6278f 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -142,6 +142,8 @@ export class LoginUriView implements View { targetUri: string, equivalentDomains: Set, defaultUriMatch: UriMatchStrategySetting = null, + /** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */ + overrideNeverMatchStrategy?: true, ): boolean { if (!this.uri || !targetUri) { return false; @@ -150,6 +152,12 @@ export class LoginUriView implements View { let matchType = this.match ?? defaultUriMatch; matchType ??= UriMatchStrategy.Domain; + // Override the match strategy with `Domain` when it is `Never` and `overrideNeverMatchStrategy` is true. + // This is useful in scenarios when the cipher should be matched to rely other information other than autofill. + if (overrideNeverMatchStrategy && matchType === UriMatchStrategy.Never) { + matchType = UriMatchStrategy.Domain; + } + const targetDomain = Utils.getDomain(targetUri); const matchDomains = equivalentDomains.add(targetDomain); diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index d268cf4afaa..44c6ee8f2e9 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -82,12 +82,16 @@ export class LoginView extends ItemView { targetUri: string, equivalentDomains: Set, defaultUriMatch: UriMatchStrategySetting = null, + /** When present, will override the match strategy for the cipher if it is `Never` with `Domain` */ + overrideNeverMatchStrategy?: true, ): boolean { if (this.uris == null) { return false; } - return this.uris.some((uri) => uri.matchesUri(targetUri, equivalentDomains, defaultUriMatch)); + return this.uris.some((uri) => + uri.matchesUri(targetUri, equivalentDomains, defaultUriMatch, overrideNeverMatchStrategy), + ); } static fromJSON(obj: Partial>): LoginView { diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index d89a41aba1f..f6e12e71edd 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -601,6 +601,7 @@ export class CipherService implements CipherServiceAbstraction { userId: UserId, includeOtherTypes?: CipherType[], defaultMatch: UriMatchStrategySetting = null, + overrideNeverMatchStrategy?: true, ): Promise { return await firstValueFrom( this.cipherViews$(userId).pipe( @@ -612,6 +613,7 @@ export class CipherService implements CipherServiceAbstraction { url, includeOtherTypes, defaultMatch, + overrideNeverMatchStrategy, ), ), ), @@ -623,6 +625,7 @@ export class CipherService implements CipherServiceAbstraction { url: string, includeOtherTypes?: CipherType[], defaultMatch: UriMatchStrategySetting = null, + overrideNeverMatchStrategy?: true, ): Promise { if (url == null && includeOtherTypes == null) { return []; @@ -647,7 +650,13 @@ export class CipherService implements CipherServiceAbstraction { } if (cipherIsLogin) { - return CipherViewLikeUtils.matchesUri(cipher, url, equivalentDomains, defaultMatch); + return CipherViewLikeUtils.matchesUri( + cipher, + url, + equivalentDomains, + defaultMatch, + overrideNeverMatchStrategy, + ); } return false; diff --git a/libs/common/src/vault/utils/cipher-view-like-utils.ts b/libs/common/src/vault/utils/cipher-view-like-utils.ts index 1c7a4382a04..5ef1d9bdc75 100644 --- a/libs/common/src/vault/utils/cipher-view-like-utils.ts +++ b/libs/common/src/vault/utils/cipher-view-like-utils.ts @@ -174,13 +174,19 @@ export class CipherViewLikeUtils { targetUri: string, equivalentDomains: Set, defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain, + overrideNeverMatchStrategy?: true, ): boolean => { if (CipherViewLikeUtils.getType(cipher) !== CipherType.Login) { return false; } if (!this.isCipherListView(cipher)) { - return cipher.login.matchesUri(targetUri, equivalentDomains, defaultUriMatch); + return cipher.login.matchesUri( + targetUri, + equivalentDomains, + defaultUriMatch, + overrideNeverMatchStrategy, + ); } const login = this.getLogin(cipher); @@ -198,7 +204,7 @@ export class CipherViewLikeUtils { }); return loginUriViews.some((uriView) => - uriView.matchesUri(targetUri, equivalentDomains, defaultUriMatch), + uriView.matchesUri(targetUri, equivalentDomains, defaultUriMatch, overrideNeverMatchStrategy), ); };