mirror of
https://github.com/bitwarden/browser
synced 2026-02-07 04:03:29 +00:00
This reverts commit f1b9408e3f.
This commit is contained in:
committed by
GitHub
parent
42386ddd60
commit
03a60a61cb
@@ -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]>>;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user