diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index bb9ec41cc7..914938ba13 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import "core-js/proposals/explicit-resource-management"; + import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs"; import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; @@ -1290,7 +1292,7 @@ export default class MainBackground { } this.containerService.attachToGlobal(self); - await this.sdkLoadService.load(); + await this.sdkLoadService.loadAndInit(); // Only the "true" background should run migrations await this.stateService.init({ runMigrations: true }); diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts index ca41127407..409ff0dea0 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-load.service.ts @@ -60,8 +60,10 @@ async function importModule(): Promise { return (globalThis as GlobalWithWasmInit).initSdk; } -export class BrowserSdkLoadService implements SdkLoadService { - constructor(readonly logService: LogService) {} +export class BrowserSdkLoadService extends SdkLoadService { + constructor(readonly logService: LogService) { + super(); + } async load(): Promise { const startTime = performance.now(); diff --git a/apps/browser/src/popup/polyfills.ts b/apps/browser/src/popup/polyfills.ts index f76b9e632f..4bb2aa0bbe 100644 --- a/apps/browser/src/popup/polyfills.ts +++ b/apps/browser/src/popup/polyfills.ts @@ -1,2 +1,3 @@ import "core-js/stable"; +import "core-js/proposals/explicit-resource-management"; import "zone.js"; diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 2ca25d690f..fe6fba85a4 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -32,7 +32,7 @@ export class InitService { init() { return async () => { - await this.sdkLoadService.load(); + await this.sdkLoadService.loadAndInit(); await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); this.twoFactorService.init(); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 5e9d3dfbc9..fcf0ef3dc8 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -1,3 +1,5 @@ +import "core-js/proposals/explicit-resource-management"; + import { program } from "commander"; import { OssServeConfigurator } from "./oss-serve-configurator"; diff --git a/apps/cli/src/platform/services/cli-sdk-load.service.ts b/apps/cli/src/platform/services/cli-sdk-load.service.ts index ee3b48e34d..638e64a821 100644 --- a/apps/cli/src/platform/services/cli-sdk-load.service.ts +++ b/apps/cli/src/platform/services/cli-sdk-load.service.ts @@ -1,7 +1,7 @@ import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import * as sdk from "@bitwarden/sdk-internal"; -export class CliSdkLoadService implements SdkLoadService { +export class CliSdkLoadService extends SdkLoadService { async load(): Promise { const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); (sdk as any).init(module); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 98926f7ae6..fcf18fd508 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -867,7 +867,7 @@ export class ServiceContainer { return; } - await this.sdkLoadService.load(); + await this.sdkLoadService.loadAndInit(); await this.storageService.init(); await this.stateService.init(); this.containerService.attachToGlobal(global); diff --git a/apps/desktop/src/app/main.ts b/apps/desktop/src/app/main.ts index ba964177db..16d03aefbd 100644 --- a/apps/desktop/src/app/main.ts +++ b/apps/desktop/src/app/main.ts @@ -1,3 +1,5 @@ +import "core-js/proposals/explicit-resource-management"; + import { enableProdMode } from "@angular/core"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 6a58f36cfb..5e00e40636 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -54,7 +54,7 @@ export class InitService { init() { return async () => { - await this.sdkLoadService.load(); + await this.sdkLoadService.loadAndInit(); await this.sshAgentService.init(); this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c6e074ead9..7e417e8e5a 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,5 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import "core-js/proposals/explicit-resource-management"; + import * as path from "path"; import { app } from "electron"; diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index 3623d9b0d2..307eed4c1e 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -42,7 +42,7 @@ export class InitService { init() { return async () => { - await this.sdkLoadService.load(); + await this.sdkLoadService.loadAndInit(); await this.stateService.init(); const activeAccount = await firstValueFrom(this.accountService.activeAccount$); diff --git a/apps/web/src/app/platform/web-sdk-load.service.ts b/apps/web/src/app/platform/web-sdk-load.service.ts index cae3399b81..8be3d20b0a 100644 --- a/apps/web/src/app/platform/web-sdk-load.service.ts +++ b/apps/web/src/app/platform/web-sdk-load.service.ts @@ -18,7 +18,7 @@ const supported = (() => { return false; })(); -export class WebSdkLoadService implements SdkLoadService { +export class WebSdkLoadService extends SdkLoadService { async load(): Promise { let module: any; if (supported) { diff --git a/apps/web/src/polyfills.ts b/apps/web/src/polyfills.ts index 33af553f78..3971ed3207 100644 --- a/apps/web/src/polyfills.ts +++ b/apps/web/src/polyfills.ts @@ -1,4 +1,5 @@ import "core-js/stable"; +import "core-js/proposals/explicit-resource-management"; import "zone.js"; if (process.env.NODE_ENV === "production") { 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 16482e797b..fb443d6177 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk-load.service.ts @@ -1,3 +1,52 @@ -export abstract class SdkLoadService { - abstract load(): Promise; +import { init_sdk } from "@bitwarden/sdk-internal"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs +import type { SdkService } from "./sdk.service"; + +export class SdkLoadFailedError extends Error { + constructor(error: unknown) { + super(`SDK loading failed: ${error}`); + } +} + +export abstract class SdkLoadService { + private static markAsReady: () => void; + private static markAsFailed: (error: unknown) => void; + + /** + * This promise is resolved when the SDK is ready to be used. Use it when your code might run early and/or is not able to use DI. + * Beware that WASM always requires a load step which makes it tricky to use functions and classes directly, it is therefore recommended + * to use the SDK through the {@link SdkService}. Only use this promise in advanced scenarios! + * + * @example + * ```typescript + * import { pureFunction } from "@bitwarden/sdk-internal"; + * + * async function myFunction() { + * await SdkLoadService.Ready; + * pureFunction(); + * } + * ``` + */ + static readonly Ready = new Promise((resolve, reject) => { + SdkLoadService.markAsReady = resolve; + SdkLoadService.markAsFailed = (error: unknown) => reject(new SdkLoadFailedError(error)); + }); + + /** + * Load WASM and initalize SDK-JS integrations such as logging. + * 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 { + try { + await this.load(); + init_sdk(); + SdkLoadService.markAsReady(); + } catch (error) { + SdkLoadService.markAsFailed(error); + } + } + + protected abstract load(): Promise; } diff --git a/libs/common/src/platform/misc/reference-counting/rc.spec.ts b/libs/common/src/platform/misc/reference-counting/rc.spec.ts index 094abfe361..f8767242ba 100644 --- a/libs/common/src/platform/misc/reference-counting/rc.spec.ts +++ b/libs/common/src/platform/misc/reference-counting/rc.spec.ts @@ -1,11 +1,3 @@ -// Temporary workaround for Symbol.dispose -// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released* -const disposeSymbol: unique symbol = Symbol("Symbol.dispose"); -const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose"); -(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"]; -(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"]; - -// Import needs to be after the workaround import { Rc } from "./rc"; export class FreeableTestValue { diff --git a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts index eff641f035..0c4114b879 100644 --- a/libs/common/src/platform/services/sdk/default-sdk-load.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk-load.service.ts @@ -8,7 +8,7 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; * * **Warning**: This requires WASM support and will fail if the environment does not support it. */ -export class DefaultSdkLoadService implements SdkLoadService { +export class DefaultSdkLoadService extends SdkLoadService { async load(): Promise { (sdk as any).init(bitwardenModule); } diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts index fed4746acd..a66b2a9cb6 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -11,6 +11,7 @@ import { UserKey } from "../../../types/key"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; import { UserNotLoggedInError } from "../../abstractions/sdk/sdk.service"; import { Rc } from "../../misc/reference-counting/rc"; import { EncryptedString } from "../../models/domain/enc-string"; @@ -18,6 +19,13 @@ import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { DefaultSdkService } from "./default-sdk.service"; +class TestSdkLoadService extends SdkLoadService { + protected override load(): Promise { + // Simulate successfull WASM load + return Promise.resolve(); + } +} + describe("DefaultSdkService", () => { describe("userClient$", () => { let sdkClientFactory!: MockProxy; @@ -28,7 +36,9 @@ describe("DefaultSdkService", () => { let keyService!: MockProxy; let service!: DefaultSdkService; - beforeEach(() => { + beforeEach(async () => { + await new TestSdkLoadService().loadAndInit(); + sdkClientFactory = mock(); environmentService = mock(); platformUtilsService = mock(); diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index 96a1dedf17..5c381c7dd1 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -18,7 +18,6 @@ import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key import { BitwardenClient, ClientSettings, - LogLevel, DeviceType as SdkDeviceType, } from "@bitwarden/sdk-internal"; @@ -30,6 +29,7 @@ import { UserKey } from "../../../types/key"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; +import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; import { SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.service"; import { compareValues } from "../../misc/compare-values"; import { Rc } from "../../misc/reference-counting/rc"; @@ -47,8 +47,9 @@ export class DefaultSdkService implements SdkService { client$ = this.environmentService.environment$.pipe( concatMap(async (env) => { + await SdkLoadService.Ready; const settings = this.toSettings(env); - return await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); + return await this.sdkClientFactory.createSdkClient(settings); }), shareReplay({ refCount: true, bufferSize: 1 }), ); @@ -135,6 +136,7 @@ export class DefaultSdkService implements SdkService { privateKey$, userKey$, orgKeys$, + SdkLoadService.Ready, // Makes sure we wait (once) for the SDK to be loaded ]).pipe( // switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value. switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => { @@ -146,7 +148,7 @@ export class DefaultSdkService implements SdkService { } const settings = this.toSettings(env); - const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); + const client = await this.sdkClientFactory.createSdkClient(settings); await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); diff --git a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts index 60dac4f21f..9fd04fdf83 100644 --- a/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts +++ b/libs/common/src/platform/services/sdk/noop-sdk-load.service.ts @@ -2,6 +2,6 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service"; export class NoopSdkLoadService extends SdkLoadService { async load() { - return; + throw new Error("SDK not available in this environment"); } } diff --git a/libs/common/test.setup.ts b/libs/common/test.setup.ts index aa71b3e508..9087c15c6b 100644 --- a/libs/common/test.setup.ts +++ b/libs/common/test.setup.ts @@ -1,3 +1,5 @@ +import "core-js/proposals/explicit-resource-management"; + import { webcrypto } from "crypto"; import { addCustomMatchers } from "./spec"; diff --git a/package-lock.json b/package-lock.json index 1431f31daa..ab526f2730 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.105", + "@bitwarden/sdk-internal": "0.2.0-main.107", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", @@ -4651,9 +4651,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.2.0-main.105", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.105.tgz", - "integrity": "sha512-MaQFJbuKTCbN9oZC/+opYVeegaNNJpiUv9/zx+gu8KxWmX0hyEkNPtHKxBjDt3kLLz69CudDtUxEgqOfcDsYAw==", + "version": "0.2.0-main.107", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.107.tgz", + "integrity": "sha512-xpOF6NAS0/em3jFBv4FI1ASy1Nuc7I1v41TVmG56wS+80y+NH1RnfGjp+a+XiO7Xxh3jssrxmjzihJjWQQA0rg==", "license": "GPL-3.0" }, "node_modules/@bitwarden/send-ui": { diff --git a/package.json b/package.json index 54b1f64208..cb941238fc 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "@angular/platform-browser": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13", "@angular/router": "18.2.13", - "@bitwarden/sdk-internal": "0.2.0-main.105", + "@bitwarden/sdk-internal": "0.2.0-main.107", "@electron/fuses": "1.8.0", "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", diff --git a/tsconfig.json b/tsconfig.json index 37f7aac05d..fb50f1e703 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "noImplicitAny": true, "target": "ES2016", "module": "ES2020", - "lib": ["es5", "es6", "es7", "dom", "ES2021"], + "lib": ["es5", "es6", "es7", "dom", "ES2021", "ESNext.Disposable"], "sourceMap": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true,