From 5dfaf032191f69f9a1d8526edc2b762ba62da805 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 20 Jan 2026 16:06:49 +0100 Subject: [PATCH] feat-wip: migrate sdk and ssh services --- apps/desktop/src/app/services/init.service.ts | 14 ++------ .../src/app/services/services.module.ts | 4 +++ .../autofill/services/ssh-agent.service.ts | 3 +- libs/angular/src/jslib.module.ts | 8 ++--- .../decentralized-init.service.ts | 25 +++------------ .../decentralized-init.service.example.ts | 14 ++++---- ...efault-decentralized-init.service.spec.ts} | 32 +++++++++---------- ... => default-decentralized-init.service.ts} | 15 +++++---- .../platform/abstractions/initializable.ts | 29 +++++++++++++++++ .../abstractions/sdk/sdk-load.service.ts | 6 ++-- 10 files changed, 81 insertions(+), 69 deletions(-) rename libs/angular/src/platform/services/{decentralized-init.service.spec.ts => default-decentralized-init.service.spec.ts} (87%) rename libs/angular/src/platform/services/{decentralized-init.service.ts => default-decentralized-init.service.ts} (86%) create mode 100644 libs/common/src/platform/abstractions/initializable.ts diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 5fb1133258a..d75bc2aecae 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -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[] = []; + 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); diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 66ba390aeee..72b96df46cd 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -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({ diff --git a/apps/desktop/src/autofill/services/ssh-agent.service.ts b/apps/desktop/src/autofill/services/ssh-agent.service.ts index 7e289720ec8..5ae38a5bc60 100644 --- a/apps/desktop/src/autofill/services/ssh-agent.service.ts +++ b/apps/desktop/src/autofill/services/ssh-agent.service.ts @@ -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; diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index bbbaaa67fbc..f01458346e6 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -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, }), ], diff --git a/libs/angular/src/platform/abstractions/decentralized-init.service.ts b/libs/angular/src/platform/abstractions/decentralized-init.service.ts index ecb2b174491..65155d051b9 100644 --- a/libs/angular/src/platform/abstractions/decentralized-init.service.ts +++ b/libs/angular/src/platform/abstractions/decentralized-init.service.ts @@ -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[]; - - /** - * Initialize this service. Called after all dependencies have been initialized. - * Can be async or sync. - */ - abstract init(): Promise | 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("INIT_SERVICES" * * @param type The Initializable service class */ -export function initializableProvider>(ctor: T) { +export function initializableProvider(ctor: T) { return { provide: INIT_SERVICES, useExisting: ctor, diff --git a/libs/angular/src/platform/services/decentralized-init.service.example.ts b/libs/angular/src/platform/services/decentralized-init.service.example.ts index 6a9d5bb4f0f..90235d8c353 100644 --- a/libs/angular/src/platform/services/decentralized-init.service.example.ts +++ b/libs/angular/src/platform/services/decentralized-init.service.example.ts @@ -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(); diff --git a/libs/angular/src/platform/services/decentralized-init.service.spec.ts b/libs/angular/src/platform/services/default-decentralized-init.service.spec.ts similarity index 87% rename from libs/angular/src/platform/services/decentralized-init.service.spec.ts rename to libs/angular/src/platform/services/default-decentralized-init.service.spec.ts index 000ce69245c..6822f1d77bc 100644 --- a/libs/angular/src/platform/services/decentralized-init.service.spec.ts +++ b/libs/angular/src/platform/services/default-decentralized-init.service.spec.ts @@ -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]; serviceB.dependencies = [ServiceA as Type]; - 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]; serviceC.dependencies = [ServiceA as Type]; - 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(); diff --git a/libs/angular/src/platform/services/decentralized-init.service.ts b/libs/angular/src/platform/services/default-decentralized-init.service.ts similarity index 86% rename from libs/angular/src/platform/services/decentralized-init.service.ts rename to libs/angular/src/platform/services/default-decentralized-init.service.ts index fa76a26769b..895b6c3c5af 100644 --- a/libs/angular/src/platform/services/decentralized-init.service.ts +++ b/libs/angular/src/platform/services/default-decentralized-init.service.ts @@ -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 { @@ -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, Initializable>(); + const instanceMap = new Map(); for (const service of services) { - instanceMap.set(service.constructor as Type, 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) { diff --git a/libs/common/src/platform/abstractions/initializable.ts b/libs/common/src/platform/abstractions/initializable.ts new file mode 100644 index 00000000000..72ceec1b13f --- /dev/null +++ b/libs/common/src/platform/abstractions/initializable.ts @@ -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. + * 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; +} diff --git a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts index 946b9894d2b..adec2f6842b 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts @@ -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 { + async init(): Promise { try { await this.load(); init_sdk(SdkLoadService.logLevel);