mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-18039] Add initial verison of IpcServices to client (#13373)
* feat: add foreground ipc service * refactor: create abstract ipc service in libs * wip: remove IPC service complexity The code was making some wrong assumptions about how IPC is going to work. I'm removing everything and starting the content-script instead * feat: working message sending from page to background * refactor: move into common * feat: somewhat complete web <-> browser link * wip: ping command from web * fix: import path * fix: wip urls * wip: add console log * feat: successfull message sending (not receiving) * feat: implement IPC using new refactored framework * wip: add some console logs * wip: almost working ping/pong * feat: working ping/pong * chore: clean-up ping/pong and some console logs * chore: remove unused file * fix: override lint rule * chore: remove unused ping message * feat: add tests for message queue * fix: adapt to name changes and modifications to SDK branch * fix: missing import * fix: remove content script from manifest The feature is not ready for prodution code yet. We will add dynamic injection with feature-flag support in a follow-up PR * fix: remove fileless lp * fix: make same changes to manifest v2 * fix: initialization functions Add missing error handling, wait for the SDK to load and properly depend on the log service * feat: use named id field * chore: update sdk version to include IPC changes * fix: remove messages$ buffer * fix: forgot to commit package-lock * feat: add additional destination check * feat: only import type in ipc-message * fix: typing issues * feat: check message origin
This commit is contained in:
@@ -111,6 +111,7 @@ import {
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
@@ -259,6 +260,7 @@ import { BackgroundBrowserBiometricsService } from "../key-management/biometrics
|
||||
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { IpcBackgroundService } from "../platform/ipc/ipc-background.service";
|
||||
import { UpdateBadge } from "../platform/listeners/update-badge";
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
|
||||
@@ -403,6 +405,8 @@ export default class MainBackground {
|
||||
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
|
||||
taskService: TaskService;
|
||||
|
||||
ipcService: IpcService;
|
||||
|
||||
onUpdatedRan: boolean;
|
||||
onReplacedRan: boolean;
|
||||
loginToAutoFill: CipherView = null;
|
||||
@@ -1309,6 +1313,8 @@ export default class MainBackground {
|
||||
);
|
||||
|
||||
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||
|
||||
this.ipcService = new IpcBackgroundService(this.logService);
|
||||
}
|
||||
|
||||
async bootstrap() {
|
||||
@@ -1382,6 +1388,7 @@ export default class MainBackground {
|
||||
}
|
||||
|
||||
await this.initOverlayAndTabsBackground();
|
||||
await this.ipcService.init();
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IpcMessage, isIpcMessage } from "@bitwarden/common/platform/ipc";
|
||||
import { MessageQueue } from "@bitwarden/common/platform/ipc/message-queue";
|
||||
import { CommunicationBackend, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
export class BackgroundCommunicationBackend implements CommunicationBackend {
|
||||
private queue = new MessageQueue<IncomingMessage>();
|
||||
|
||||
constructor() {
|
||||
BrowserApi.messageListener("platform.ipc", (message, sender) => {
|
||||
if (!isIpcMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender.tab?.id === undefined || sender.tab.id === chrome.tabs.TAB_ID_NONE) {
|
||||
// Ignore messages from non-tab sources
|
||||
return;
|
||||
}
|
||||
|
||||
void this.queue.enqueue({ ...message.message, source: { Web: { id: sender.tab.id } } });
|
||||
});
|
||||
}
|
||||
|
||||
async send(message: OutgoingMessage): Promise<void> {
|
||||
if (typeof message.destination === "object" && "Web" in message.destination) {
|
||||
await BrowserApi.tabSendMessage(
|
||||
{ id: message.destination.Web.id } as chrome.tabs.Tab,
|
||||
{ type: "bitwarden-ipc-message", message } satisfies IpcMessage,
|
||||
{ frameId: 0 },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Destination not supported.");
|
||||
}
|
||||
|
||||
async receive(): Promise<IncomingMessage> {
|
||||
return this.queue.dequeue();
|
||||
}
|
||||
}
|
||||
51
apps/browser/src/platform/ipc/content/ipc-content-script.ts
Normal file
51
apps/browser/src/platform/ipc/content/ipc-content-script.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// TODO: This content script should be dynamically reloaded when the extension is updated,
|
||||
// to avoid "Extension context invalidated." errors.
|
||||
|
||||
import { isIpcMessage } from "@bitwarden/common/platform/ipc/ipc-message";
|
||||
|
||||
// Web -> Background
|
||||
export function sendExtensionMessage(message: unknown) {
|
||||
if (
|
||||
typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.sendMessage !== "undefined"
|
||||
) {
|
||||
void browser.runtime.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
void chrome.runtime.sendMessage(message);
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.origin !== window.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isIpcMessage(event.data)) {
|
||||
sendExtensionMessage(event.data);
|
||||
}
|
||||
});
|
||||
|
||||
// Background -> Web
|
||||
function setupMessageListener() {
|
||||
function listener(message: unknown) {
|
||||
if (isIpcMessage(message)) {
|
||||
void window.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.onMessage !== "undefined"
|
||||
) {
|
||||
browser.runtime.onMessage.addListener(listener);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax -- This doesn't run in the popup but in the content script
|
||||
chrome.runtime.onMessage.addListener(listener);
|
||||
}
|
||||
|
||||
setupMessageListener();
|
||||
26
apps/browser/src/platform/ipc/ipc-background.service.ts
Normal file
26
apps/browser/src/platform/ipc/ipc-background.service.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { IpcClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { BackgroundCommunicationBackend } from "./background-communication-backend";
|
||||
|
||||
export class IpcBackgroundService extends IpcService {
|
||||
private communicationProvider?: BackgroundCommunicationBackend;
|
||||
|
||||
constructor(private logService: LogService) {
|
||||
super();
|
||||
}
|
||||
|
||||
override async init() {
|
||||
try {
|
||||
// This function uses classes and functions defined in the SDK, so we need to wait for the SDK to load.
|
||||
await SdkLoadService.Ready;
|
||||
this.communicationProvider = new BackgroundCommunicationBackend();
|
||||
|
||||
await super.initWithClient(new IpcClient(this.communicationProvider));
|
||||
} catch (e) {
|
||||
this.logService.error("[IPC] Initialization failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,7 @@ const mainConfig = {
|
||||
"content/content-message-handler": "./src/autofill/content/content-message-handler.ts",
|
||||
"content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts",
|
||||
"content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts",
|
||||
"content/ipc-content-script": "./src/platform/ipc/content/ipc-content-script.ts",
|
||||
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||
"overlay/menu-button":
|
||||
"./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts",
|
||||
|
||||
@@ -75,6 +75,7 @@ import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sd
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import {
|
||||
UnsupportedWebPushConnectionService,
|
||||
@@ -122,9 +123,11 @@ import { WebSsoComponentService } from "../auth/core/services/login/web-sso-comp
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
import { WebFileDownloadService } from "../core/web-file-download.service";
|
||||
import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service";
|
||||
import { WebProcessReloadService } from "../key-management/services/web-process-reload.service";
|
||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
import { WebIpcService } from "../platform/ipc/web-ipc.service";
|
||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||
import { WebSdkLoadService } from "../platform/web-sdk-load.service";
|
||||
@@ -135,7 +138,6 @@ import { InitService } from "./init.service";
|
||||
import { ENV_URLS } from "./injection-tokens";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { RouterService } from "./router.service";
|
||||
import { WebFileDownloadService } from "./web-file-download.service";
|
||||
import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
||||
|
||||
/**
|
||||
@@ -368,6 +370,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebLoginDecryptionOptionsService,
|
||||
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IpcService,
|
||||
useClass: WebIpcService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SshImportPromptService,
|
||||
useClass: DefaultSshImportPromptService,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
@@ -38,6 +39,7 @@ export class InitService {
|
||||
private userAutoUnlockKeyService: UserAutoUnlockKeyService,
|
||||
private accountService: AccountService,
|
||||
private versionService: VersionService,
|
||||
private ipcService: IpcService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@@ -72,6 +74,7 @@ export class InitService {
|
||||
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
|
||||
this.themingService.applyThemeChangesTo(this.document);
|
||||
this.versionService.applyVersionToWindow();
|
||||
void this.ipcService.init();
|
||||
|
||||
const containerService = new ContainerService(this.keyService, this.encryptService);
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
||||
41
apps/web/src/app/platform/ipc/web-communication-provider.ts
Normal file
41
apps/web/src/app/platform/ipc/web-communication-provider.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { IpcMessage, isIpcMessage } from "@bitwarden/common/platform/ipc";
|
||||
import { MessageQueue } from "@bitwarden/common/platform/ipc/message-queue";
|
||||
import { CommunicationBackend, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class WebCommunicationProvider implements CommunicationBackend {
|
||||
private queue = new MessageQueue<IncomingMessage>();
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("message", async (event: MessageEvent) => {
|
||||
if (event.origin !== window.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data;
|
||||
if (!isIpcMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.queue.enqueue({ ...message.message, source: "BrowserBackground" });
|
||||
});
|
||||
}
|
||||
|
||||
async send(message: OutgoingMessage): Promise<void> {
|
||||
if (message.destination === "BrowserBackground") {
|
||||
window.postMessage(
|
||||
{ type: "bitwarden-ipc-message", message } satisfies IpcMessage,
|
||||
window.location.origin,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Destination not supported: ${message.destination}`);
|
||||
}
|
||||
|
||||
receive(): Promise<IncomingMessage> {
|
||||
return this.queue.dequeue();
|
||||
}
|
||||
}
|
||||
25
apps/web/src/app/platform/ipc/web-ipc.service.ts
Normal file
25
apps/web/src/app/platform/ipc/web-ipc.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { inject } from "@angular/core";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { IpcClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { WebCommunicationProvider } from "./web-communication-provider";
|
||||
|
||||
export class WebIpcService extends IpcService {
|
||||
private logService = inject(LogService);
|
||||
private communicationProvider?: WebCommunicationProvider;
|
||||
|
||||
override async init() {
|
||||
try {
|
||||
// This function uses classes and functions defined in the SDK, so we need to wait for the SDK to load.
|
||||
await SdkLoadService.Ready;
|
||||
this.communicationProvider = new WebCommunicationProvider();
|
||||
|
||||
await super.initWithClient(new IpcClient(this.communicationProvider));
|
||||
} catch (e) {
|
||||
this.logService.error("[IPC] Initialization failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
libs/common/src/platform/ipc/index.ts
Normal file
2
libs/common/src/platform/ipc/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ipc-message";
|
||||
export * from "./ipc.service";
|
||||
10
libs/common/src/platform/ipc/ipc-message.ts
Normal file
10
libs/common/src/platform/ipc/ipc-message.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
export interface IpcMessage {
|
||||
type: "bitwarden-ipc-message";
|
||||
message: OutgoingMessage;
|
||||
}
|
||||
|
||||
export function isIpcMessage(message: any): message is IpcMessage {
|
||||
return message.type === "bitwarden-ipc-message";
|
||||
}
|
||||
51
libs/common/src/platform/ipc/ipc.service.ts
Normal file
51
libs/common/src/platform/ipc/ipc.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Observable, shareReplay } from "rxjs";
|
||||
|
||||
import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
export abstract class IpcService {
|
||||
private _client?: IpcClient;
|
||||
protected get client(): IpcClient {
|
||||
if (!this._client) {
|
||||
throw new Error("IpcService not initialized");
|
||||
}
|
||||
return this._client;
|
||||
}
|
||||
|
||||
private _messages$?: Observable<IncomingMessage>;
|
||||
protected get messages$(): Observable<IncomingMessage> {
|
||||
if (!this._messages$) {
|
||||
throw new Error("IpcService not initialized");
|
||||
}
|
||||
return this._messages$;
|
||||
}
|
||||
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
protected async initWithClient(client: IpcClient): Promise<void> {
|
||||
this._client = client;
|
||||
this._messages$ = new Observable<IncomingMessage>((subscriber) => {
|
||||
let isSubscribed = true;
|
||||
|
||||
const receiveLoop = async () => {
|
||||
while (isSubscribed) {
|
||||
try {
|
||||
const message = await this.client.receive();
|
||||
subscriber.next(message);
|
||||
} catch (error) {
|
||||
subscriber.error(error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
void receiveLoop();
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
};
|
||||
}).pipe(shareReplay({ bufferSize: 0, refCount: true }));
|
||||
}
|
||||
|
||||
async send(message: OutgoingMessage) {
|
||||
await this.client.send(message);
|
||||
}
|
||||
}
|
||||
48
libs/common/src/platform/ipc/message-queue.spec.ts
Normal file
48
libs/common/src/platform/ipc/message-queue.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { MessageQueue } from "./message-queue";
|
||||
|
||||
type Message = symbol;
|
||||
|
||||
describe("MessageQueue", () => {
|
||||
let messageQueue!: MessageQueue<Message>;
|
||||
|
||||
beforeEach(() => {
|
||||
messageQueue = new MessageQueue<Message>();
|
||||
});
|
||||
|
||||
it("waits for a new message when queue is empty", async () => {
|
||||
const message = createMessage();
|
||||
|
||||
// Start a promise to dequeue a message
|
||||
let dequeuedValue: Message | undefined;
|
||||
void messageQueue.dequeue().then((value) => {
|
||||
dequeuedValue = value;
|
||||
});
|
||||
|
||||
// No message is enqueued yet
|
||||
expect(dequeuedValue).toBeUndefined();
|
||||
|
||||
// Enqueue a message
|
||||
await messageQueue.enqueue(message);
|
||||
|
||||
// Expect the message to be dequeued
|
||||
await new Promise(process.nextTick);
|
||||
expect(dequeuedValue).toBe(message);
|
||||
});
|
||||
|
||||
it("returns existing message when queue is not empty", async () => {
|
||||
const message = createMessage();
|
||||
|
||||
// Enqueue a message
|
||||
await messageQueue.enqueue(message);
|
||||
|
||||
// Dequeue the message
|
||||
const dequeuedValue = await messageQueue.dequeue();
|
||||
|
||||
// Expect the message to be dequeued
|
||||
expect(dequeuedValue).toBe(message);
|
||||
});
|
||||
});
|
||||
|
||||
function createMessage(name?: string): symbol {
|
||||
return Symbol(name);
|
||||
}
|
||||
20
libs/common/src/platform/ipc/message-queue.ts
Normal file
20
libs/common/src/platform/ipc/message-queue.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
|
||||
export class MessageQueue<T> {
|
||||
private queue: T[] = [];
|
||||
private messageAvailable$ = new Subject<void>();
|
||||
|
||||
async enqueue(message: T): Promise<void> {
|
||||
this.queue.push(message);
|
||||
this.messageAvailable$.next();
|
||||
}
|
||||
|
||||
async dequeue(): Promise<T> {
|
||||
if (this.queue.length > 0) {
|
||||
return this.queue.shift() as T;
|
||||
}
|
||||
|
||||
await firstValueFrom(this.messageAvailable$);
|
||||
return this.queue.shift() as T;
|
||||
}
|
||||
}
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -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.124",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.133",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "3.0.2",
|
||||
@@ -4699,9 +4699,10 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/sdk-internal": {
|
||||
"version": "0.2.0-main.124",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.124.tgz",
|
||||
"integrity": "sha512-7F+DlPFng/thT4EVIQk2tRC7kff6G2B7alHAIxBdioJc9vE64Z5R5pviUyMZzqLnA5e9y8EnQdtWsQzUkHxisQ=="
|
||||
"version": "0.2.0-main.133",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.133.tgz",
|
||||
"integrity": "sha512-KzKJGf9cKlcQzfRmqkAwVGBN1kDpcRFkTMm7nrphZSrjfaWJWI1lBEJ0DhnkbMMHJXhQavGyoVk5TIn/Y8ylmw==",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
"node_modules/@bitwarden/send-ui": {
|
||||
"resolved": "libs/tools/send/send-ui",
|
||||
|
||||
@@ -155,7 +155,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.124",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.133",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "3.0.2",
|
||||
|
||||
Reference in New Issue
Block a user