From c5975f02e651b3d8262a3373640ebeb3885899b8 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 21 Jan 2026 17:20:01 -0800 Subject: [PATCH] Move getCipher functions to cipher-sdk.service.ts when using SDK flag --- .../vault/abstractions/cipher-sdk.service.ts | 32 +++++ .../vault/services/cipher-sdk.service.spec.ts | 133 ++++++++++++++++++ .../src/vault/services/cipher-sdk.service.ts | 78 +++++++++- .../src/vault/services/cipher.service.spec.ts | 119 +++++----------- .../src/vault/services/cipher.service.ts | 105 ++++---------- 5 files changed, 301 insertions(+), 166 deletions(-) diff --git a/libs/common/src/vault/abstractions/cipher-sdk.service.ts b/libs/common/src/vault/abstractions/cipher-sdk.service.ts index 3101531eda6..805a30221ca 100644 --- a/libs/common/src/vault/abstractions/cipher-sdk.service.ts +++ b/libs/common/src/vault/abstractions/cipher-sdk.service.ts @@ -1,6 +1,16 @@ import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +/** + * Result of decrypting all ciphers, containing both successes and failures. + */ +export interface DecryptAllCiphersResult { + /** Successfully decrypted cipher views */ + successes: CipherView[]; + /** Cipher views that failed to decrypt (with decryptionFailure flag set) */ + failures: CipherView[]; +} + /** * Service responsible for cipher operations using the SDK. */ @@ -106,4 +116,26 @@ export abstract class CipherSdkService { * @returns A promise that resolves when the ciphers are restored */ abstract restoreManyWithServer(ids: string[], userId: UserId, orgId?: string): Promise; + + /** + * Lists and decrypts all ciphers from state using the SDK. + * + * @param userId The user ID to use for SDK client + * @returns A promise that resolves to the decrypt result containing successes and failures + */ + abstract getAllDecrypted(userId: UserId): Promise; + + /** + * Fetches and decrypts all ciphers for an organization from the API using the SDK. + * + * @param organizationId The organization ID to fetch ciphers for + * @param userId The user ID to use for SDK client + * @param includeMemberItems Whether to include member items + * @returns A promise that resolves to the decrypted cipher views + */ + abstract getAllFromApiForOrganization( + organizationId: string, + userId: UserId, + includeMemberItems: boolean, + ): Promise; } diff --git a/libs/common/src/vault/services/cipher-sdk.service.spec.ts b/libs/common/src/vault/services/cipher-sdk.service.spec.ts index cb21ff28133..7640627b774 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.spec.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.spec.ts @@ -34,6 +34,7 @@ describe("DefaultCipherSdkService", () => { soft_delete_many: jest.fn().mockResolvedValue(undefined), restore: jest.fn().mockResolvedValue(undefined), restore_many: jest.fn().mockResolvedValue(undefined), + list_org_ciphers: jest.fn().mockResolvedValue({ successes: [], failures: [] }), }; mockCiphersSdk = { create: jest.fn(), @@ -44,6 +45,7 @@ describe("DefaultCipherSdkService", () => { soft_delete_many: jest.fn().mockResolvedValue(undefined), restore: jest.fn().mockResolvedValue(undefined), restore_many: jest.fn().mockResolvedValue(undefined), + list: jest.fn().mockResolvedValue({ successes: [], failures: [] }), admin: jest.fn().mockReturnValue(mockAdminSdk), }; mockVaultSdk = { @@ -531,4 +533,135 @@ describe("DefaultCipherSdkService", () => { ); }); }); + + describe("getAllDecrypted()", () => { + it("should list and decrypt ciphers using SDK", async () => { + const mockSdkCipherView = new CipherView().toSdkCipherView(); + mockSdkCipherView.name = "Test Cipher"; + mockCiphersSdk.list.mockResolvedValue({ + successes: [mockSdkCipherView], + failures: [], + }); + + const result = await cipherSdkService.getAllDecrypted(userId); + + expect(sdkService.userClient$).toHaveBeenCalledWith(userId); + expect(mockVaultSdk.ciphers).toHaveBeenCalled(); + expect(mockCiphersSdk.list).toHaveBeenCalled(); + expect(result.successes).toHaveLength(1); + expect(result.successes[0]).toBeInstanceOf(CipherView); + expect(result.failures).toHaveLength(0); + }); + + it("should return failures with decryptionFailure flag set", async () => { + // Create a minimal mock that matches what fromSdkCipher expects + const mockFailedCipher: any = { + id: cipherId, + name: "2.encryptedName|iv|data", + type: CipherType.Login, + organizationId: null, + folderId: null, + favorite: false, + edit: true, + viewPassword: true, + organizationUseTotp: false, + revisionDate: new Date().toISOString(), + collectionIds: [], + deletedDate: null, + reprompt: 0, + key: null, + localData: null, + attachments: null, + fields: null, + passwordHistory: null, + creationDate: new Date().toISOString(), + login: null, + secureNote: null, + card: null, + identity: null, + sshKey: null, + }; + mockCiphersSdk.list.mockResolvedValue({ + successes: [], + failures: [mockFailedCipher], + }); + + const result = await cipherSdkService.getAllDecrypted(userId); + + expect(result.successes).toHaveLength(0); + expect(result.failures).toHaveLength(1); + expect(result.failures[0].decryptionFailure).toBe(true); + }); + + it("should throw error and log when SDK client is not available", async () => { + sdkService.userClient$.mockReturnValue(of(null)); + + await expect(cipherSdkService.getAllDecrypted(userId)).rejects.toThrow("SDK not available"); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to list and decrypt ciphers"), + ); + }); + + it("should throw error and log when SDK throws an error", async () => { + mockCiphersSdk.list.mockRejectedValue(new Error("SDK error")); + + await expect(cipherSdkService.getAllDecrypted(userId)).rejects.toThrow(); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to list and decrypt ciphers"), + ); + }); + }); + + describe("getAllFromApiForOrganization()", () => { + it("should list organization ciphers using SDK admin API", async () => { + const mockSdkCipherView = new CipherView().toSdkCipherView(); + mockSdkCipherView.name = "Org Cipher"; + mockAdminSdk.list_org_ciphers.mockResolvedValue({ + successes: [mockSdkCipherView], + failures: [], + }); + + const result = await cipherSdkService.getAllFromApiForOrganization(orgId, userId, false); + + expect(sdkService.userClient$).toHaveBeenCalledWith(userId); + expect(mockVaultSdk.ciphers).toHaveBeenCalled(); + expect(mockCiphersSdk.admin).toHaveBeenCalled(); + expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(orgId, false); + expect(result).toHaveLength(1); + expect(result[0]).toBeInstanceOf(CipherView); + }); + + it("should pass includeMemberItems parameter to SDK", async () => { + mockAdminSdk.list_org_ciphers.mockResolvedValue({ + successes: [], + failures: [], + }); + + await cipherSdkService.getAllFromApiForOrganization(orgId, userId, true); + + expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(orgId, true); + }); + + it("should throw error and log when SDK client is not available", async () => { + sdkService.userClient$.mockReturnValue(of(null)); + + await expect( + cipherSdkService.getAllFromApiForOrganization(orgId, userId, false), + ).rejects.toThrow("SDK not available"); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to list organization ciphers"), + ); + }); + + it("should throw error and log when SDK throws an error", async () => { + mockAdminSdk.list_org_ciphers.mockRejectedValue(new Error("SDK error")); + + await expect( + cipherSdkService.getAllFromApiForOrganization(orgId, userId, false), + ).rejects.toThrow(); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to list organization ciphers"), + ); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher-sdk.service.ts b/libs/common/src/vault/services/cipher-sdk.service.ts index 9757b3d2cc7..8e60fb6e2c7 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.ts @@ -1,12 +1,14 @@ import { firstValueFrom, switchMap, catchError } from "rxjs"; +import { DECRYPT_ERROR } from "@bitwarden/common/key-management/crypto/models/enc-string"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SdkService, asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; -import { CipherSdkService } from "../abstractions/cipher-sdk.service"; +import { CipherSdkService, DecryptAllCiphersResult } from "../abstractions/cipher-sdk.service"; +import { Cipher } from "../models/domain/cipher"; export class DefaultCipherSdkService implements CipherSdkService { constructor( @@ -260,4 +262,78 @@ export class DefaultCipherSdkService implements CipherSdkService { ), ); } + + async getAllDecrypted(userId: UserId): Promise { + return await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + switchMap(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + + const decryptResult = await ref.value.vault().ciphers().list(); + + // Convert successes - SDK returns array of SdkCipherView + const successArray = Array.isArray(decryptResult.successes) + ? decryptResult.successes + : Array.from(decryptResult.successes ?? []); + + const successes = successArray + .map((sdkCipherView: any) => CipherView.fromSdkCipherView(sdkCipherView)) + .filter((v): v is CipherView => v !== undefined); + + // Convert failures to CipherView with error markers + const failureArray = Array.isArray(decryptResult.failures) + ? decryptResult.failures + : Array.from(decryptResult.failures ?? []); + + const failures: CipherView[] = failureArray.map((failure: any) => { + const cipher = Cipher.fromSdkCipher(failure); + const cipherView = new CipherView(cipher); + cipherView.name = DECRYPT_ERROR; + cipherView.decryptionFailure = true; + return cipherView; + }); + + return { successes, failures }; + }), + catchError((error: unknown) => { + this.logService.error(`Failed to list and decrypt ciphers: ${error}`); + throw error; + }), + ), + ); + } + + async getAllFromApiForOrganization( + organizationId: string, + userId: UserId, + includeMemberItems: boolean, + ): Promise { + return await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + switchMap(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + + const decryptResult = await ref.value + .vault() + .ciphers() + .admin() + .list_org_ciphers(asUuid(organizationId), includeMemberItems); + + return decryptResult.successes + .map((sdkCipherView: any) => CipherView.fromSdkCipherView(sdkCipherView)) + .filter((v): v is CipherView => v !== undefined); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to list organization ciphers: ${error}`); + throw error; + }), + ), + ); + } } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 0b1a7a1ad61..f15e2af7a2b 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1258,47 +1258,7 @@ describe("Cipher Service", () => { }); describe("getAllFromApiForOrganization()", () => { - let mockSdkClient: any; - let mockCiphersSdk: any; - let mockAdminSdk: any; - let mockVaultSdk: any; const testOrgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b21" as OrganizationId; - const mockSdkCipherView1 = { - id: "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22", - name: "Test Cipher 1", - }; - const mockSdkCipherView2 = { - id: "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23", - name: "Test Cipher 2", - }; - - beforeEach(() => { - // Mock the SDK client chain for list_org_ciphers - mockAdminSdk = { - list_org_ciphers: jest.fn().mockResolvedValue({ - successes: [mockSdkCipherView1, mockSdkCipherView2], - failures: [], - }), - }; - mockCiphersSdk = { - admin: jest.fn().mockReturnValue(mockAdminSdk), - }; - mockVaultSdk = { - ciphers: jest.fn().mockReturnValue(mockCiphersSdk), - }; - const mockSdkValue = { - vault: jest.fn().mockReturnValue(mockVaultSdk), - }; - mockSdkClient = { - take: jest.fn().mockReturnValue({ - value: mockSdkValue, - [Symbol.dispose]: jest.fn(), - }), - }; - - // Mock sdkService to return the mock client - sdkService.userClient$.mockReturnValue(of(mockSdkClient)); - }); it("should call apiService.getCiphersOrganization when feature flag is disabled", async () => { configService.getFeatureFlag @@ -1316,7 +1276,6 @@ describe("Cipher Service", () => { await cipherService.getAllFromApiForOrganization(testOrgId, true); expect(apiSpy).toHaveBeenCalledWith(testOrgId, true); - expect(mockSdkClient.take).not.toHaveBeenCalled(); }); it("should call apiService.getCiphersOrganization without includeMemberItems when not provided", async () => { @@ -1332,7 +1291,6 @@ describe("Cipher Service", () => { await cipherService.getAllFromApiForOrganization(testOrgId); expect(apiSpy).toHaveBeenCalledWith(testOrgId, undefined); - expect(mockSdkClient.take).not.toHaveBeenCalled(); }); it("should use SDK to list organization ciphers when feature flag is enabled", async () => { @@ -1340,12 +1298,20 @@ describe("Cipher Service", () => { .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) .mockResolvedValue(true); + const mockCipherView1 = new CipherView(); + mockCipherView1.name = "Test Cipher 1"; + const mockCipherView2 = new CipherView(); + mockCipherView2.name = "Test Cipher 2"; + + const sdkServiceSpy = jest + .spyOn(cipherSdkService, "getAllFromApiForOrganization") + .mockResolvedValue([mockCipherView1, mockCipherView2]); + const apiSpy = jest.spyOn(apiService, "getCiphersOrganization"); const result = await cipherService.getAllFromApiForOrganization(testOrgId, true); - expect(mockSdkClient.take).toHaveBeenCalled(); - expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(testOrgId, true); + expect(sdkServiceSpy).toHaveBeenCalledWith(testOrgId, mockUserId, true); expect(apiSpy).not.toHaveBeenCalled(); expect(result).toHaveLength(2); expect(result[0]).toBeInstanceOf(CipherView); @@ -1357,53 +1323,21 @@ describe("Cipher Service", () => { .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) .mockResolvedValue(true); + const sdkServiceSpy = jest + .spyOn(cipherSdkService, "getAllFromApiForOrganization") + .mockResolvedValue([]); + const apiSpy = jest.spyOn(apiService, "getCiphersOrganization"); await cipherService.getAllFromApiForOrganization(testOrgId); - expect(mockSdkClient.take).toHaveBeenCalled(); - expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(testOrgId, false); + expect(sdkServiceSpy).toHaveBeenCalledWith(testOrgId, mockUserId, false); expect(apiSpy).not.toHaveBeenCalled(); }); }); describe("getAllDecrypted()", () => { - let mockSdkClient: any; - let mockCiphersSdk: any; - let mockVaultSdk: any; - const mockSdkCipherView1 = { - id: "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22", - name: "Test Cipher 1", - }; - const mockSdkCipherView2 = { - id: "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23", - name: "Test Cipher 2", - }; - beforeEach(() => { - // Mock the SDK client chain for list - mockCiphersSdk = { - list: jest.fn().mockResolvedValue({ - successes: [mockSdkCipherView1, mockSdkCipherView2], - failures: [], - }), - }; - mockVaultSdk = { - ciphers: jest.fn().mockReturnValue(mockCiphersSdk), - }; - const mockSdkValue = { - vault: jest.fn().mockReturnValue(mockVaultSdk), - }; - mockSdkClient = { - take: jest.fn().mockReturnValue({ - value: mockSdkValue, - [Symbol.dispose]: jest.fn(), - }), - }; - - // Mock sdkService to return the mock client - sdkService.userClient$.mockReturnValue(of(mockSdkClient)); - // Clear the decrypted cache to ensure we test the decrypt path stateProvider.singleUser.getFake(mockUserId, DECRYPTED_CIPHERS).nextState({}); }); @@ -1413,21 +1347,32 @@ describe("Cipher Service", () => { .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) .mockResolvedValue(true); + const mockCipherView1 = new CipherView(); + mockCipherView1.name = "Test Cipher 1"; + const mockCipherView2 = new CipherView(); + mockCipherView2.name = "Test Cipher 2"; + + const sdkServiceSpy = jest.spyOn(cipherSdkService, "getAllDecrypted").mockResolvedValue({ + successes: [mockCipherView1, mockCipherView2], + failures: [], + }); + const result = await cipherService.getAllDecrypted(mockUserId); - expect(mockSdkClient.take).toHaveBeenCalled(); - expect(mockCiphersSdk.list).toHaveBeenCalled(); + expect(sdkServiceSpy).toHaveBeenCalledWith(mockUserId); expect(result).toHaveLength(2); expect(result[0]).toBeInstanceOf(CipherView); expect(result[1]).toBeInstanceOf(CipherView); }); - it("should not call SDK when feature flag is disabled", async () => { + it("should not call cipherSdkService when feature flag is disabled", async () => { configService.getFeatureFlag .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) .mockResolvedValue(false); - // Just verify SDK is not called - don't test the full legacy path + const sdkServiceSpy = jest.spyOn(cipherSdkService, "getAllDecrypted"); + + // Just verify SDK service is not called - don't test the full legacy path // as it would require complex mocking of keyService observables stateProvider.singleUser.getFake(mockUserId, ENCRYPTED_CIPHERS).nextState({}); @@ -1435,10 +1380,10 @@ describe("Cipher Service", () => { await cipherService.getAllDecrypted(mockUserId); } catch { // Expected to fail due to missing keyService mocks, but that's okay - // We just want to verify SDK wasn't called + // We just want to verify SDK service wasn't called } - expect(mockSdkClient.take).not.toHaveBeenCalled(); + expect(sdkServiceSpy).not.toHaveBeenCalled(); }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 320d993dd97..505a087d704 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -6,7 +6,6 @@ import { firstValueFrom, map, Observable, - of, Subject, switchMap, tap, @@ -26,7 +25,7 @@ import { AutofillSettingsServiceAbstraction } from "../../autofill/services/auto import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { FeatureFlag } from "../../enums/feature-flag.enum"; import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service"; -import { DECRYPT_ERROR, EncString } from "../../key-management/crypto/models/enc-string"; +import { EncString } from "../../key-management/crypto/models/enc-string"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; @@ -490,7 +489,7 @@ export class CipherService implements CipherServiceAbstraction { FeatureFlag.PM27632_SdkCipherCrudOperations, ); if (useSdk) { - return this.getAllDecrypted_sdk(userId); + return this.getAllDecryptedUsingSdk(userId); } const decCiphers = await this.getDecryptedCiphers(userId); @@ -514,53 +513,18 @@ export class CipherService implements CipherServiceAbstraction { return newDecCiphers; } - private async getAllDecrypted_sdk(userId: UserId): Promise { - // Use SDK to list and decrypt all ciphers from state - const result = await firstValueFrom( - this.sdkService.userClient$(userId).pipe( - switchMap(async (sdk) => { - if (!sdk) { - throw new Error("SDK not available"); - } - using ref = sdk.take(); + private async getAllDecryptedUsingSdk(userId: UserId): Promise { + try { + const result = await this.cipherSdkService.getAllDecrypted(userId); - const decryptResult = await ref.value.vault().ciphers().list(); + await this.setDecryptedCipherCache(result.successes, userId); + await this.setFailedDecryptedCiphers(result.failures, userId); - // Convert successes - SDK returns array of SdkCipherView - const successArray = Array.isArray(decryptResult.successes) - ? decryptResult.successes - : Array.from(decryptResult.successes ?? []); - - const successViews = successArray.map((sdkCipherView: any) => - CipherView.fromSdkCipherView(sdkCipherView), - ); - - // Convert failures to CipherView with error markers - const failureArray = Array.isArray(decryptResult.failures) - ? decryptResult.failures - : Array.from(decryptResult.failures ?? []); - - const failureViews: CipherView[] = failureArray.map((failure: any) => { - const cipher = Cipher.fromSdkCipher(failure); - const cipherView = new CipherView(cipher); - cipherView.name = DECRYPT_ERROR; - cipherView.decryptionFailure = true; - return cipherView; - }); - - await this.setDecryptedCipherCache(successViews, userId); - await this.setFailedDecryptedCiphers(failureViews, userId); - - return successViews; - }), - catchError((error: unknown) => { - this.logService.error(`Failed to list and decrypt ciphers: ${error}`); - return of([]); - }), - ), - ); - - return result; + return result.successes; + } catch { + // Return empty array on error to maintain existing behavior + return []; + } } private async getDecryptedCiphers(userId: UserId) { @@ -801,7 +765,7 @@ export class CipherService implements CipherServiceAbstraction { FeatureFlag.PM27632_SdkCipherCrudOperations, ); if (useSdk) { - return this.getAllFromApiForOrganization_sdk(organizationId, includeMemberItems ?? false); + return this.getAllFromApiForOrganizationUsingSdk(organizationId, includeMemberItems ?? false); } const response = await this.apiService.getCiphersOrganization( @@ -811,7 +775,7 @@ export class CipherService implements CipherServiceAbstraction { return await this.decryptOrganizationCiphersResponse(response, organizationId); } - private async getAllFromApiForOrganization_sdk( + private async getAllFromApiForOrganizationUsingSdk( organizationId: string, includeMemberItems: boolean, ): Promise { @@ -820,36 +784,21 @@ export class CipherService implements CipherServiceAbstraction { throw new Error("User ID is required"); } - const result = await firstValueFrom( - this.sdkService.userClient$(userId).pipe( - switchMap(async (sdk) => { - if (!sdk) { - throw new Error("SDK not available"); - } - using ref = sdk.take(); - const decryptResult = await ref.value - .vault() - .ciphers() - .admin() - .list_org_ciphers(asUuid(organizationId), includeMemberItems); + try { + const cipherViews = await this.cipherSdkService.getAllFromApiForOrganization( + organizationId, + userId, + includeMemberItems, + ); - const cipherViews = decryptResult.successes.map((sdkCipherView: any) => - CipherView.fromSdkCipherView(sdkCipherView), - ); + // Sort by locale (matching existing behavior) + cipherViews.sort(this.getLocaleSortingFunction()); - // Sort by locale (matching existing behavior) - cipherViews.sort(this.getLocaleSortingFunction()); - - return cipherViews; - }), - catchError((error: unknown) => { - this.logService.error(`Failed to list organization ciphers: ${error}`); - return of([]); - }), - ), - ); - - return result; + return cipherViews; + } catch { + // Return empty array on error to maintain existing behavior + return []; + } } async getManyFromApiForOrganization(organizationId: string): Promise {