From 03a60a61cbcf420441462d3d4e25440365332ae8 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:15:12 +0100 Subject: [PATCH] Revert "[PM-29149] Add ServerCommunicationConfigService (#18815)" (#18821) This reverts commit f1b9408e3f0101fa5e24e058b0c7880ae9c06314. --- .../server-communication-config.service.ts | 27 --- ...erver-communication-config.service.spec.ts | 146 ------------- ...ult-server-communication-config.service.ts | 47 ----- .../server-communication-config/index.ts | 3 - ...er-communication-config.repository.spec.ts | 193 ------------------ .../server-communication-config.repository.ts | 57 ------ .../server-communication-config.state.ts | 18 -- libs/state/src/core/state-definitions.ts | 4 - 8 files changed, 495 deletions(-) delete mode 100644 libs/common/src/platform/abstractions/server-communication-config/server-communication-config.service.ts delete mode 100644 libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.spec.ts delete mode 100644 libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.ts delete mode 100644 libs/common/src/platform/services/server-communication-config/index.ts delete mode 100644 libs/common/src/platform/services/server-communication-config/server-communication-config.repository.spec.ts delete mode 100644 libs/common/src/platform/services/server-communication-config/server-communication-config.repository.ts delete mode 100644 libs/common/src/platform/services/server-communication-config/server-communication-config.state.ts diff --git a/libs/common/src/platform/abstractions/server-communication-config/server-communication-config.service.ts b/libs/common/src/platform/abstractions/server-communication-config/server-communication-config.service.ts deleted file mode 100644 index 19afebaa516..00000000000 --- a/libs/common/src/platform/abstractions/server-communication-config/server-communication-config.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Observable } from "rxjs"; - -/** - * Service for managing server communication configuration, - * including bootstrap detection and cookie management. - */ -export abstract class ServerCommunicationConfigService { - /** - * Observable that emits true when the specified hostname - * requires bootstrap (cookie acquisition) before API calls can succeed. - * - * Automatically updates when server communication config state changes. - * - * @param hostname - The server hostname (e.g., "vault.acme.com") - * @returns Observable that emits bootstrap status for the hostname - */ - abstract needsBootstrap$(hostname: string): Observable; - - /** - * Retrieves cookies that should be included in HTTP requests - * to the specified hostname. - * - * @param hostname - The server hostname - * @returns Promise resolving to array of [cookie_name, cookie_value] tuples - */ - abstract getCookies(hostname: string): Promise>; -} diff --git a/libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.spec.ts b/libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.spec.ts deleted file mode 100644 index 8e565d7ee1c..00000000000 --- a/libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { firstValueFrom } from "rxjs"; - -import { ServerCommunicationConfig } from "@bitwarden/sdk-internal"; - -import { awaitAsync, FakeAccountService, FakeStateProvider } from "../../../../spec"; - -import { DefaultServerCommunicationConfigService } from "./default-server-communication-config.service"; -import { ServerCommunicationConfigRepository } from "./server-communication-config.repository"; - -// Mock SDK client -jest.mock("@bitwarden/sdk-internal", () => ({ - ServerCommunicationConfigClient: jest.fn().mockImplementation(() => ({ - needsBootstrap: jest.fn(), - cookies: jest.fn(), - getConfig: jest.fn(), - })), -})); - -describe("DefaultServerCommunicationConfigService", () => { - let stateProvider: FakeStateProvider; - let repository: ServerCommunicationConfigRepository; - let service: DefaultServerCommunicationConfigService; - let mockClient: any; - - beforeEach(() => { - const accountService = new FakeAccountService({}); - stateProvider = new FakeStateProvider(accountService); - repository = new ServerCommunicationConfigRepository(stateProvider); - service = new DefaultServerCommunicationConfigService(repository); - mockClient = (service as any).client; - }); - - describe("needsBootstrap$", () => { - it("emits false when direct bootstrap configured", async () => { - mockClient.needsBootstrap.mockResolvedValue(false); - - const result = await firstValueFrom(service.needsBootstrap$("vault.bitwarden.com")); - - expect(result).toBe(false); - expect(mockClient.needsBootstrap).toHaveBeenCalledWith("vault.bitwarden.com"); - }); - - it("emits true when SSO cookie vendor bootstrap needed", async () => { - mockClient.needsBootstrap.mockResolvedValue(true); - - const result = await firstValueFrom(service.needsBootstrap$("vault.acme.com")); - - expect(result).toBe(true); - expect(mockClient.needsBootstrap).toHaveBeenCalledWith("vault.acme.com"); - }); - - it("re-emits when config state changes", async () => { - mockClient.needsBootstrap.mockResolvedValueOnce(false).mockResolvedValueOnce(true); - - const observable = service.needsBootstrap$("vault.bitwarden.com"); - const emissions: boolean[] = []; - - // Subscribe to collect emissions - const subscription = observable.subscribe((value) => emissions.push(value)); - - // Wait for first emission - await awaitAsync(); - expect(emissions[0]).toBe(false); - - // Update config state to trigger re-check - const config: ServerCommunicationConfig = { - bootstrap: { type: "direct" }, - }; - await repository.save("vault.bitwarden.com", config); - - // Wait for second emission - await awaitAsync(); - expect(emissions[1]).toBe(true); - - subscription.unsubscribe(); - }); - - it("creates independent observables per hostname", async () => { - mockClient.needsBootstrap.mockImplementation(async (hostname: string) => { - return hostname === "vault1.acme.com"; - }); - - const result1 = await firstValueFrom(service.needsBootstrap$("vault1.acme.com")); - const result2 = await firstValueFrom(service.needsBootstrap$("vault2.acme.com")); - - expect(result1).toBe(true); - expect(result2).toBe(false); - expect(mockClient.needsBootstrap).toHaveBeenCalledWith("vault1.acme.com"); - expect(mockClient.needsBootstrap).toHaveBeenCalledWith("vault2.acme.com"); - }); - - it("shares result between simultaneous subscribers", async () => { - mockClient.needsBootstrap.mockResolvedValue(true); - - const observable = service.needsBootstrap$("vault.bitwarden.com"); - - // Multiple simultaneous subscribers should share the same call - const [result1, result2] = await Promise.all([ - firstValueFrom(observable), - firstValueFrom(observable), - ]); - - expect(result1).toBe(true); - expect(result2).toBe(true); - // Should only call once for simultaneous subscribers - expect(mockClient.needsBootstrap).toHaveBeenCalledTimes(1); - }); - }); - - describe("getCookies", () => { - it("retrieves cookies for hostname", async () => { - const expectedCookies: Array<[string, string]> = [ - ["auth_token", "abc123"], - ["session_id", "xyz789"], - ]; - mockClient.cookies.mockResolvedValue(expectedCookies); - - const result = await service.getCookies("vault.bitwarden.com"); - - expect(result).toEqual(expectedCookies); - expect(mockClient.cookies).toHaveBeenCalledWith("vault.bitwarden.com"); - }); - - it("returns empty array when no cookies configured", async () => { - mockClient.cookies.mockResolvedValue([]); - - const result = await service.getCookies("vault.bitwarden.com"); - - expect(result).toEqual([]); - expect(mockClient.cookies).toHaveBeenCalledWith("vault.bitwarden.com"); - }); - - it("handles different hostnames independently", async () => { - mockClient.cookies - .mockResolvedValueOnce([["cookie1", "value1"]]) - .mockResolvedValueOnce([["cookie2", "value2"]]); - - const result1 = await service.getCookies("vault1.acme.com"); - const result2 = await service.getCookies("vault2.acme.com"); - - expect(result1).toEqual([["cookie1", "value1"]]); - expect(result2).toEqual([["cookie2", "value2"]]); - expect(mockClient.cookies).toHaveBeenCalledTimes(2); - }); - }); -}); diff --git a/libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.ts b/libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.ts deleted file mode 100644 index b9194981622..00000000000 --- a/libs/common/src/platform/services/server-communication-config/default-server-communication-config.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Observable, shareReplay, switchMap } from "rxjs"; - -import { ServerCommunicationConfigClient } from "@bitwarden/sdk-internal"; - -import { ServerCommunicationConfigService } from "../../abstractions/server-communication-config/server-communication-config.service"; - -import { ServerCommunicationConfigRepository } from "./server-communication-config.repository"; - -/** - * Default implementation of ServerCommunicationConfigService. - * - * Manages server communication configuration and bootstrap detection for different - * server environments. Provides reactive observables that automatically respond to - * configuration changes and integrate with the SDK's ServerCommunicationConfigClient. - * - * @remarks - * Bootstrap detection determines if SSO cookie acquisition is required before - * API calls can succeed. The service watches for configuration changes and - * re-evaluates bootstrap requirements automatically. - * - * Key features: - * - Reactive observables for bootstrap status (`needsBootstrap$`) - * - Per-hostname configuration management - * - Automatic re-evaluation when config state changes - * - Cookie retrieval for HTTP request headers - * - */ -export class DefaultServerCommunicationConfigService implements ServerCommunicationConfigService { - private client: ServerCommunicationConfigClient; - - constructor(private repository: ServerCommunicationConfigRepository) { - // Initialize SDK client with repository - this.client = new ServerCommunicationConfigClient(repository); - } - - needsBootstrap$(hostname: string): Observable { - // Watch hostname-specific config changes and re-check when it updates - return this.repository.get$(hostname).pipe( - switchMap(() => this.client.needsBootstrap(hostname)), - shareReplay({ refCount: true, bufferSize: 1 }), - ); - } - - async getCookies(hostname: string): Promise> { - return this.client.cookies(hostname); - } -} diff --git a/libs/common/src/platform/services/server-communication-config/index.ts b/libs/common/src/platform/services/server-communication-config/index.ts deleted file mode 100644 index 7d6bac1c067..00000000000 --- a/libs/common/src/platform/services/server-communication-config/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ServerCommunicationConfigRepository } from "./server-communication-config.repository"; -export { DefaultServerCommunicationConfigService } from "./default-server-communication-config.service"; -export { SERVER_COMMUNICATION_CONFIGS } from "./server-communication-config.state"; diff --git a/libs/common/src/platform/services/server-communication-config/server-communication-config.repository.spec.ts b/libs/common/src/platform/services/server-communication-config/server-communication-config.repository.spec.ts deleted file mode 100644 index 2ed16e96c11..00000000000 --- a/libs/common/src/platform/services/server-communication-config/server-communication-config.repository.spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { ServerCommunicationConfig } from "@bitwarden/sdk-internal"; - -import { FakeAccountService, FakeStateProvider } from "../../../../spec"; - -import { ServerCommunicationConfigRepository } from "./server-communication-config.repository"; - -describe("ServerCommunicationConfigRepository", () => { - let stateProvider: FakeStateProvider; - let repository: ServerCommunicationConfigRepository; - - beforeEach(() => { - const accountService = new FakeAccountService({}); - stateProvider = new FakeStateProvider(accountService); - repository = new ServerCommunicationConfigRepository(stateProvider); - }); - - it("returns undefined when no config exists for hostname", async () => { - const result = await repository.get("vault.acme.com"); - - expect(result).toBeUndefined(); - }); - - it("saves and retrieves a direct bootstrap config", async () => { - const config: ServerCommunicationConfig = { - bootstrap: { type: "direct" }, - }; - - await repository.save("vault.acme.com", config); - const result = await repository.get("vault.acme.com"); - - expect(result).toEqual(config); - }); - - it("saves and retrieves an SSO cookie vendor config", async () => { - const config: ServerCommunicationConfig = { - bootstrap: { - type: "ssoCookieVendor", - idp_login_url: "https://idp.example.com/login", - cookie_name: "auth_token", - cookie_domain: ".acme.com", - cookie_value: "abc123", - }, - }; - - await repository.save("vault.acme.com", config); - const result = await repository.get("vault.acme.com"); - - expect(result).toEqual(config); - }); - - it("handles SSO config with undefined cookie_value", async () => { - const config: ServerCommunicationConfig = { - bootstrap: { - type: "ssoCookieVendor", - idp_login_url: "https://idp.example.com/login", - cookie_name: "auth_token", - cookie_domain: ".acme.com", - cookie_value: undefined, - }, - }; - - await repository.save("vault.acme.com", config); - const result = await repository.get("vault.acme.com"); - - expect(result).toEqual(config); - }); - - it("overwrites existing config for same hostname", async () => { - const initialConfig: ServerCommunicationConfig = { - bootstrap: { type: "direct" }, - }; - await repository.save("vault.acme.com", initialConfig); - - const newConfig: ServerCommunicationConfig = { - bootstrap: { - type: "ssoCookieVendor", - idp_login_url: "https://idp.example.com", - cookie_name: "token", - cookie_domain: ".acme.com", - cookie_value: "xyz789", - }, - }; - await repository.save("vault.acme.com", newConfig); - - const result = await repository.get("vault.acme.com"); - expect(result).toEqual(newConfig); - }); - - it("maintains separate configs for different hostnames", async () => { - const config1: ServerCommunicationConfig = { - bootstrap: { type: "direct" }, - }; - const config2: ServerCommunicationConfig = { - bootstrap: { - type: "ssoCookieVendor", - idp_login_url: "https://idp.example.com", - cookie_name: "token", - cookie_domain: ".example.com", - cookie_value: "token123", - }, - }; - - await repository.save("vault1.acme.com", config1); - await repository.save("vault2.example.com", config2); - - const result1 = await repository.get("vault1.acme.com"); - const result2 = await repository.get("vault2.example.com"); - - expect(result1).toEqual(config1); - expect(result2).toEqual(config2); - }); - - describe("get$", () => { - it("emits undefined for hostname with no config", (done) => { - repository.get$("vault.acme.com").subscribe((config) => { - expect(config).toBeUndefined(); - done(); - }); - }); - - it("emits config when it exists", async () => { - const config: ServerCommunicationConfig = { - bootstrap: { type: "direct" }, - }; - - await repository.save("vault.acme.com", config); - - repository.get$("vault.acme.com").subscribe((result) => { - expect(result).toEqual(config); - }); - }); - - it("emits new value when config changes", (done) => { - const config1: ServerCommunicationConfig = { - bootstrap: { type: "direct" }, - }; - const config2: ServerCommunicationConfig = { - bootstrap: { - type: "ssoCookieVendor", - idp_login_url: "https://idp.example.com", - cookie_name: "token", - cookie_domain: ".acme.com", - cookie_value: "abc123", - }, - }; - - const emissions: (ServerCommunicationConfig | undefined)[] = []; - const subscription = repository.get$("vault.acme.com").subscribe((config) => { - emissions.push(config); - - if (emissions.length === 3) { - expect(emissions[0]).toBeUndefined(); - expect(emissions[1]).toEqual(config1); - expect(emissions[2]).toEqual(config2); - subscription.unsubscribe(); - done(); - } - }); - - // Trigger updates - setTimeout(async () => { - await repository.save("vault.acme.com", config1); - setTimeout(async () => { - await repository.save("vault.acme.com", config2); - }, 10); - }, 10); - }); - - it("only emits when the specific hostname config changes", (done) => { - const config1: ServerCommunicationConfig = { - bootstrap: { type: "direct" }, - }; - - const emissions: (ServerCommunicationConfig | undefined)[] = []; - const subscription = repository.get$("vault1.acme.com").subscribe((config) => { - emissions.push(config); - }); - - setTimeout(async () => { - // Save config for different hostname - should not trigger emission for vault1 - await repository.save("vault2.acme.com", config1); - - setTimeout(() => { - // Only initial undefined emission should exist - expect(emissions.length).toBe(1); - expect(emissions[0]).toBeUndefined(); - subscription.unsubscribe(); - done(); - }, 50); - }, 10); - }); - }); -}); diff --git a/libs/common/src/platform/services/server-communication-config/server-communication-config.repository.ts b/libs/common/src/platform/services/server-communication-config/server-communication-config.repository.ts deleted file mode 100644 index 7ca38be1e6e..00000000000 --- a/libs/common/src/platform/services/server-communication-config/server-communication-config.repository.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { distinctUntilChanged, firstValueFrom, map, Observable } from "rxjs"; - -import { - ServerCommunicationConfigRepository as SdkRepository, - ServerCommunicationConfig, -} from "@bitwarden/sdk-internal"; - -import { GlobalState, StateProvider } from "../../state"; - -import { SERVER_COMMUNICATION_CONFIGS } from "./server-communication-config.state"; - -/** - * Implementation of SDK-defined interface. - * Bridges the SDK's repository abstraction with StateProvider for persistence. - * - * This repository manages server communication configurations keyed by hostname, - * storing information about bootstrap requirements (direct vs SSO cookie vendor) - * for each server environment. - * - * @remarks - * - Uses global state (application-level, not user-scoped) - * - Configurations persist across sessions (stored on disk) - * - Each hostname maintains independent configuration - * - All error handling is performed by the SDK caller - * - */ -export class ServerCommunicationConfigRepository implements SdkRepository { - private state: GlobalState>; - - constructor(private stateProvider: StateProvider) { - this.state = this.stateProvider.getGlobal(SERVER_COMMUNICATION_CONFIGS); - } - - async get(hostname: string): Promise { - return firstValueFrom(this.get$(hostname)); - } - - /** - * Observable that emits when the configuration for a specific hostname changes. - * - * @param hostname - The server hostname - * @returns Observable that emits the config for the hostname, or undefined if not set - */ - get$(hostname: string): Observable { - return this.state.state$.pipe( - map((configs) => configs?.[hostname]), - distinctUntilChanged(), - ); - } - - async save(hostname: string, config: ServerCommunicationConfig): Promise { - await this.state.update((configs) => ({ - ...configs, - [hostname]: config, - })); - } -} diff --git a/libs/common/src/platform/services/server-communication-config/server-communication-config.state.ts b/libs/common/src/platform/services/server-communication-config/server-communication-config.state.ts deleted file mode 100644 index 65bc692df5f..00000000000 --- a/libs/common/src/platform/services/server-communication-config/server-communication-config.state.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ServerCommunicationConfig } from "@bitwarden/sdk-internal"; - -import { KeyDefinition, SERVER_COMMUNICATION_CONFIG_DISK } from "../../state"; - -/** - * Key definition for server communication configurations. - * - * Record type: Maps hostname (string) to ServerCommunicationConfig - * Storage: Disk (persisted across sessions) - * Scope: Global (application-level, not user-specific) - */ -export const SERVER_COMMUNICATION_CONFIGS = KeyDefinition.record( - SERVER_COMMUNICATION_CONFIG_DISK, - "configs", - { - deserializer: (value: ServerCommunicationConfig) => value, - }, -); diff --git a/libs/state/src/core/state-definitions.ts b/libs/state/src/core/state-definitions.ts index f9113b7e64e..ae6938b2069 100644 --- a/libs/state/src/core/state-definitions.ts +++ b/libs/state/src/core/state-definitions.ts @@ -137,10 +137,6 @@ export const EXTENSION_INITIAL_INSTALL_DISK = new StateDefinition( export const WEB_PUSH_SUBSCRIPTION = new StateDefinition("webPushSubscription", "disk", { web: "disk-local", }); -export const SERVER_COMMUNICATION_CONFIG_DISK = new StateDefinition( - "serverCommunicationConfig", - "disk", -); // Design System