1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00

Revert "[PM-29149] Add ServerCommunicationConfigService (#18815)" (#18821)

This reverts commit f1b9408e3f.
This commit is contained in:
Daniel James Smith
2026-02-06 22:15:12 +01:00
committed by GitHub
parent 42386ddd60
commit 03a60a61cb
8 changed files with 0 additions and 495 deletions

View File

@@ -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<boolean>;
/**
* 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<Array<[string, string]>>;
}

View File

@@ -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);
});
});
});

View File

@@ -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<boolean> {
// 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<Array<[string, string]>> {
return this.client.cookies(hostname);
}
}

View File

@@ -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";

View File

@@ -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);
});
});
});

View File

@@ -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<Record<string, ServerCommunicationConfig>>;
constructor(private stateProvider: StateProvider) {
this.state = this.stateProvider.getGlobal(SERVER_COMMUNICATION_CONFIGS);
}
async get(hostname: string): Promise<ServerCommunicationConfig | undefined> {
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<ServerCommunicationConfig | undefined> {
return this.state.state$.pipe(
map((configs) => configs?.[hostname]),
distinctUntilChanged(),
);
}
async save(hostname: string, config: ServerCommunicationConfig): Promise<void> {
await this.state.update((configs) => ({
...configs,
[hostname]: config,
}));
}
}

View File

@@ -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<ServerCommunicationConfig, string>(
SERVER_COMMUNICATION_CONFIG_DISK,
"configs",
{
deserializer: (value: ServerCommunicationConfig) => value,
},
);

View File

@@ -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