1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-27 23:03:45 +00:00

feat-wip: migrate sdk and ssh services

This commit is contained in:
Andreas Coroiu
2026-01-20 16:06:49 +01:00
parent 6b30234614
commit 5dfaf03219
10 changed files with 81 additions and 69 deletions

View File

@@ -1,7 +1,6 @@
import { Inject, Injectable, DOCUMENT, Type } from "@angular/core";
import { Inject, Injectable, DOCUMENT } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { Initializable } from "@bitwarden/angular/platform/abstractions/decentralized-init.service";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
@@ -11,9 +10,9 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Dependency, Initializable } from "@bitwarden/common/platform/abstractions/initializable";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
import { ServerNotificationsService } from "@bitwarden/common/platform/server-notifications";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
@@ -42,30 +41,23 @@ export class InitService implements Initializable {
private twoFactorService: TwoFactorService,
private notificationsService: ServerNotificationsService,
private platformUtilsService: PlatformUtilsServiceAbstraction,
private stateService: StateServiceAbstraction,
private keyService: KeyServiceAbstraction,
private nativeMessagingService: NativeMessagingService,
private themingService: AbstractThemingService,
private encryptService: EncryptService,
private userAutoUnlockKeyService: UserAutoUnlockKeyService,
private accountService: AccountService,
private versionService: VersionService,
private sshAgentService: SshAgentService,
private autofillService: DesktopAutofillService,
private autotypeService: DesktopAutotypeService,
private sdkLoadService: SdkLoadService,
private biometricMessageHandlerService: BiometricMessageHandlerService,
private configService: ConfigService,
@Inject(DOCUMENT) private document: Document,
private readonly migrationRunner: MigrationRunner,
) {}
dependencies: Type<Initializable>[] = [];
dependencies: Dependency[] = [SdkLoadService, SshAgentService, NativeMessagingService];
async init() {
await this.sdkLoadService.loadAndInit();
await this.sshAgentService.init();
this.nativeMessagingService.init();
await this.migrationRunner.waitForCompletion(); // Desktop will run migrations in the main process
this.encryptService.init(this.configService);

View File

@@ -146,6 +146,7 @@ import { DesktopAutofillService } from "../../autofill/services/desktop-autofill
import { DesktopAutotypeDefaultSettingPolicy } from "../../autofill/services/desktop-autotype-policy.service";
import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service";
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
import { SshAgentService } from "../../autofill/services/ssh-agent.service";
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
import { RendererBiometricsService } from "../../key-management/biometrics/renderer-biometrics.service";
import { ElectronKeyService } from "../../key-management/electron-key.service";
@@ -562,6 +563,9 @@ const safeProviders: SafeProvider[] = [
],
}),
initializableProvider(InitService),
initializableProvider(SdkLoadService),
// initializableProvider(flagEnabled("sdk") ? DefaultSdkLoadService : NoopSdkLoadService),
initializableProvider(SshAgentService),
];
@NgModule({

View File

@@ -25,6 +25,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Initializable } from "@bitwarden/common/platform/abstractions/initializable";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
import { UserId } from "@bitwarden/common/types/guid";
@@ -39,7 +40,7 @@ import { SshAgentPromptType } from "../models/ssh-agent-setting";
@Injectable({
providedIn: "root",
})
export class SshAgentService implements OnDestroy {
export class SshAgentService implements OnDestroy, Initializable {
SSH_REFRESH_INTERVAL = 1000;
SSH_VAULT_UNLOCK_REQUEST_TIMEOUT = 60_000;
SSH_REQUEST_UNLOCK_POLLING_INTERVAL = 100;

View File

@@ -45,11 +45,11 @@ import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe";
import { SearchPipe } from "./pipes/search.pipe";
import { UserNamePipe } from "./pipes/user-name.pipe";
import { UserTypePipe } from "./pipes/user-type.pipe";
import { DecentralizedInitService as DecentralizedInitServiceAbstraction } from "./platform/abstractions/decentralized-init.service";
import { DecentralizedInitService } from "./platform/abstractions/decentralized-init.service";
import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe";
import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe";
import { I18nPipe } from "./platform/pipes/i18n.pipe";
import { DecentralizedInitService } from "./platform/services/decentralized-init.service";
import { DefaultDecentralizedInitService } from "./platform/services/default-decentralized-init.service";
import { safeProvider } from "./platform/utils/safe-provider";
import { IconComponent } from "./vault/components/icon.component";
@@ -149,8 +149,8 @@ import { IconComponent } from "./vault/components/icon.component";
FingerprintPipe,
PluralizePipe,
safeProvider({
provide: DecentralizedInitServiceAbstraction,
useClass: DecentralizedInitService,
provide: DecentralizedInitService,
useClass: DefaultDecentralizedInitService,
useAngularDecorators: true,
}),
],

View File

@@ -1,26 +1,9 @@
import { InjectionToken, Type } from "@angular/core";
import { InjectionToken } from "@angular/core";
import { Dependency, Initializable } from "@bitwarden/common/platform/abstractions/initializable";
import { SafeProvider } from "../utils/safe-provider";
/**
* Services that implement Initializable can participate in decentralized initialization.
* Each service declares its dependencies, and the DecentralizedInitService will execute
* them in the correct order using topological sort.
*/
export abstract class Initializable {
/**
* List of service classes that must be initialized before this service.
* Use actual class references for type safety and refactoring support.
*/
abstract dependencies: Type<Initializable>[];
/**
* Initialize this service. Called after all dependencies have been initialized.
* Can be async or sync.
*/
abstract init(): Promise<void> | void;
}
/**
* Multi-provider token for registering services that need initialization.
* Services register themselves by adding to their library's provider bundle:
@@ -40,7 +23,7 @@ export const INIT_SERVICES = new InjectionToken<Initializable[]>("INIT_SERVICES"
*
* @param type The Initializable service class
*/
export function initializableProvider<T extends Type<Initializable>>(ctor: T) {
export function initializableProvider<T extends Dependency>(ctor: T) {
return {
provide: INIT_SERVICES,
useExisting: ctor,

View File

@@ -1,10 +1,10 @@
/**
* Example usage of DecentralizedInitService
* Example usage of DefaultDecentralizedInitService
*
* This file demonstrates how to:
* 1. Make services implement Initializable
* 2. Register services with INIT_SERVICES
* 3. Use DecentralizedInitService in your app
* 3. Use DefaultDecentralizedInitService in your app
*
* This is NOT production code - it's a reference example.
*/
@@ -90,17 +90,17 @@ export const EXAMPLE_LIBRARY_PROVIDERS = [
/**
* In your app's main config (e.g., app.config.ts or main.ts):
*
* import { DecentralizedInitService } from '@bitwarden/angular/platform/services/decentralized-init.service';
* import { DefaultDecentralizedInitService } from '@bitwarden/angular/platform/services/default-decentralized-init.service';
* import { EXAMPLE_LIBRARY_PROVIDERS } from '@bitwarden/angular/platform/services/decentralized-init.service.example';
*
* export const appConfig: ApplicationConfig = {
* providers: [
* ...EXAMPLE_LIBRARY_PROVIDERS,
* DecentralizedInitService,
* DefaultDecentralizedInitService,
* {
* provide: APP_INITIALIZER,
* useFactory: (initService: DecentralizedInitService) => () => initService.init(),
* deps: [DecentralizedInitService],
* useFactory: (initService: DefaultDecentralizedInitService) => () => initService.init(),
* deps: [DefaultDecentralizedInitService],
* multi: true,
* },
* ]
@@ -110,7 +110,7 @@ export const EXAMPLE_LIBRARY_PROVIDERS = [
*
* @Component({ ... })
* export class AppComponent {
* constructor(private initService: DecentralizedInitService) {}
* constructor(private initService: DefaultDecentralizedInitService) {}
*
* ngOnInit() {
* await this.initService.init();

View File

@@ -2,7 +2,7 @@ import { Type } from "@angular/core";
import { Initializable } from "../abstractions/decentralized-init.service";
import { DecentralizedInitService } from "./decentralized-init.service";
import { DefaultDecentralizedInitService } from "./default-decentralized-init.service";
// Test service implementations
class TestService implements Initializable {
@@ -23,7 +23,7 @@ function createTrackingService(name: string, executionOrder: string[]) {
};
}
describe("DecentralizedInitService", () => {
describe("DefaultDecentralizedInitService", () => {
let executionOrder: string[];
beforeEach(() => {
@@ -34,7 +34,7 @@ describe("DecentralizedInitService", () => {
describe("given no registered services", () => {
it("completes without error when called", async () => {
// Arrange
const sut = new DecentralizedInitService([]);
const sut = new DefaultDecentralizedInitService([]);
// Act & Assert
await expect(sut.init()).resolves.not.toThrow();
@@ -45,7 +45,7 @@ describe("DecentralizedInitService", () => {
it("initializes a single service when called", async () => {
// Arrange
const service = new TestService();
const sut = new DecentralizedInitService([service]);
const sut = new DefaultDecentralizedInitService([service]);
// Act
await sut.init();
@@ -59,7 +59,7 @@ describe("DecentralizedInitService", () => {
const service1 = new TestService();
const service2 = new TestService();
const service3 = new TestService();
const sut = new DecentralizedInitService([service1, service2, service3]);
const sut = new DefaultDecentralizedInitService([service1, service2, service3]);
// Act
await sut.init();
@@ -81,7 +81,7 @@ describe("DecentralizedInitService", () => {
const serviceB = new ServiceB();
serviceB.dependencies = [ServiceA];
const sut = new DecentralizedInitService([serviceB, serviceA]);
const sut = new DefaultDecentralizedInitService([serviceB, serviceA]);
// Act
await sut.init();
@@ -107,7 +107,7 @@ describe("DecentralizedInitService", () => {
serviceC.dependencies = [ServiceA, ServiceB];
serviceD.dependencies = [ServiceC];
const sut = new DecentralizedInitService([serviceD, serviceB, serviceC, serviceA]);
const sut = new DefaultDecentralizedInitService([serviceD, serviceB, serviceC, serviceA]);
// Act
await sut.init();
@@ -139,7 +139,7 @@ describe("DecentralizedInitService", () => {
serviceC.dependencies = [ServiceA];
serviceD.dependencies = [ServiceB, ServiceC];
const sut = new DecentralizedInitService([serviceD, serviceC, serviceB, serviceA]);
const sut = new DefaultDecentralizedInitService([serviceD, serviceC, serviceB, serviceA]);
// Act
await sut.init();
@@ -166,7 +166,7 @@ describe("DecentralizedInitService", () => {
}
const service = new CountingService();
const sut = new DecentralizedInitService([service]);
const sut = new DefaultDecentralizedInitService([service]);
// Act
await sut.init();
@@ -188,7 +188,7 @@ describe("DecentralizedInitService", () => {
serviceA.dependencies = [ServiceB as Type<Initializable>];
serviceB.dependencies = [ServiceA as Type<Initializable>];
const sut = new DecentralizedInitService([serviceA, serviceB]);
const sut = new DefaultDecentralizedInitService([serviceA, serviceB]);
// Act & Assert
await expect(sut.init()).rejects.toThrow(/Circular dependency detected/);
@@ -208,7 +208,7 @@ describe("DecentralizedInitService", () => {
serviceB.dependencies = [ServiceC as Type<Initializable>];
serviceC.dependencies = [ServiceA as Type<Initializable>];
const sut = new DecentralizedInitService([serviceA, serviceB, serviceC]);
const sut = new DefaultDecentralizedInitService([serviceA, serviceB, serviceC]);
// Act & Assert
await expect(sut.init()).rejects.toThrow(/Circular dependency detected/);
@@ -224,7 +224,7 @@ describe("DecentralizedInitService", () => {
}
const serviceB = new ServiceB();
const sut = new DecentralizedInitService([serviceB]);
const sut = new DefaultDecentralizedInitService([serviceB]);
// Act & Assert
await expect(sut.init()).rejects.toThrow(/not registered in INIT_SERVICES/);
@@ -238,7 +238,7 @@ describe("DecentralizedInitService", () => {
}
const myService = new MyService();
const sut = new DecentralizedInitService([myService]);
const sut = new DefaultDecentralizedInitService([myService]);
// Act & Assert
await expect(sut.init()).rejects.toThrow("MyService depends on MyDependency");
@@ -256,7 +256,7 @@ describe("DecentralizedInitService", () => {
}
const service = new FailingService();
const sut = new DecentralizedInitService([service]);
const sut = new DefaultDecentralizedInitService([service]);
// Act & Assert
await expect(sut.init()).rejects.toThrow(/Failed to initialize FailingService/);
@@ -275,7 +275,7 @@ describe("DecentralizedInitService", () => {
}
const service = new SyncService();
const sut = new DecentralizedInitService([service]);
const sut = new DefaultDecentralizedInitService([service]);
// Act
await sut.init();
@@ -305,7 +305,7 @@ describe("DecentralizedInitService", () => {
const syncService = new SyncService();
const asyncService = new AsyncService();
const sut = new DecentralizedInitService([asyncService, syncService]);
const sut = new DefaultDecentralizedInitService([asyncService, syncService]);
// Act
await sut.init();

View File

@@ -1,8 +1,9 @@
import { Inject, Injectable, Type } from "@angular/core";
import { Inject, Injectable } from "@angular/core";
import { Dependency, Initializable } from "@bitwarden/common/platform/abstractions/initializable";
import {
DecentralizedInitService as DecentralizedInitServiceAbstraction,
Initializable,
DecentralizedInitService,
INIT_SERVICES,
} from "../abstractions/decentralized-init.service";
@@ -18,7 +19,7 @@ import {
* - Executes init() methods sequentially in dependency order
*/
@Injectable()
export class DecentralizedInitService implements DecentralizedInitServiceAbstraction {
export class DefaultDecentralizedInitService implements DecentralizedInitService {
constructor(@Inject(INIT_SERVICES) private initServices: Initializable[]) {}
async init(): Promise<void> {
@@ -46,9 +47,9 @@ export class DecentralizedInitService implements DecentralizedInitServiceAbstrac
*/
private topologicalSort(services: Initializable[]): Initializable[] {
// Build a map from constructor to instance for quick lookup
const instanceMap = new Map<Type<Initializable>, Initializable>();
const instanceMap = new Map<Dependency, Initializable>();
for (const service of services) {
instanceMap.set(service.constructor as Type<Initializable>, service);
instanceMap.set(service.constructor as Dependency, service);
}
const sorted: Initializable[] = [];
@@ -70,7 +71,7 @@ export class DecentralizedInitService implements DecentralizedInitServiceAbstrac
const currentPath = [...path, service.constructor.name];
// Visit all dependencies first
for (const depClass of service.dependencies) {
for (const depClass of service.dependencies ?? []) {
const depInstance = instanceMap.get(depClass);
if (!depInstance) {

View File

@@ -0,0 +1,29 @@
declare const Dependency: FunctionConstructor;
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export interface Dependency extends Function {
prototype: Initializable;
}
/**
* Services that implement Initializable can participate in decentralized initialization.
* Each service declares its dependencies, and initialization will execute them in the
* correct order using topological sort.
*
* This is a framework-agnostic abstraction that can be used across all clients.
*/
export interface Initializable {
/**
* List of service classes that must be initialized before this service.
* Use actual class references for type safety and refactoring support.
*
* Note: The exact type depends on the framework. For Angular, use Type<Initializable>.
* For non-Angular clients, use the constructor type directly.
*/
dependencies?: Dependency[];
/**
* Initialize this service. Called after all dependencies have been initialized.
* Can be async or sync.
*/
init(): Promise<void> | void;
}

View File

@@ -1,5 +1,7 @@
import { init_sdk, LogLevel } from "@bitwarden/sdk-internal";
import { Initializable } from "../initializable";
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
import type { SdkService } from "./sdk.service";
@@ -9,7 +11,7 @@ export class SdkLoadFailedError extends Error {
}
}
export abstract class SdkLoadService {
export abstract class SdkLoadService implements Initializable {
protected static logLevel: LogLevel = LogLevel.Info;
private static markAsReady: () => void;
private static markAsFailed: (error: unknown) => void;
@@ -39,7 +41,7 @@ export abstract class SdkLoadService {
* This method should be called once at the start of the application.
* Raw functions and classes from the SDK can be used after this method resolves.
*/
async loadAndInit(): Promise<void> {
async init(): Promise<void> {
try {
await this.load();
init_sdk(SdkLoadService.logLevel);