1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 04:03:29 +00:00

refactor: introduce @bitwarden/api and @bitwarden/legacy-api

- Created libs/legacy-api with description and platform team ownership
- Updated tsconfig.base path mapping and CODEOWNERS
- npm install changes in package-lock from generator

feat(api): add minimal ApiClient and HttpOperations type with lifted core methods (send, fetch/nativeFetch, token helpers, error handling)

feat(api): include prerelease flag header in ApiClient headers to match ApiService behavior

refactor(api): type logoutCallback param as LogoutReason to mirror ApiService

chore(api): remove unused HttpOperations import from abstraction

refactor(api): rename api-client.ts to api.service.ts and update barrel/spec imports

chore(api): restore comments and constructor parity with common ApiService (device field, directory traversal note, unauthenticated header rationale)

angular: provide TokenProvider -> TokenProviderService; convert TokenProvider to abstract class for DI token; no cycles with @bitwarden/api
This commit is contained in:
addisonbeck
2025-08-08 15:45:19 -04:00
parent 8d8edc0d9e
commit d509d277ee
28 changed files with 648 additions and 0 deletions

2
.github/CODEOWNERS vendored
View File

@@ -106,6 +106,8 @@ libs/state-test-utils @bitwarden/team-platform-dev
libs/device-type @bitwarden/team-platform-dev
libs/encoding @bitwarden/team-platform-dev
libs/platform-utils @bitwarden/team-platform-dev
libs/api @bitwarden/team-platform-dev
libs/legacy-api @bitwarden/team-platform-dev
# Web utils used across app and connectors
apps/web/src/utils/ @bitwarden/team-platform-dev
# Web core and shared files

5
libs/api/README.md Normal file
View File

@@ -0,0 +1,5 @@
# api
Owned by: platform
Library for Bitwarden API access

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

10
libs/api/jest.config.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
displayName: "api",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/api",
};

11
libs/api/package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/api",
"version": "0.0.1",
"description": "Library for Bitwarden API access",
"private": true,
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "GPL-3.0",
"author": "platform"
}

33
libs/api/project.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "api",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/api/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/api",
"main": "libs/api/src/index.ts",
"tsConfig": "libs/api/tsconfig.lib.json",
"assets": ["libs/api/*.md"]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/api/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/api/jest.config.js"
}
}
}
}

View File

@@ -0,0 +1,33 @@
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
export interface ApiServiceAbstraction {
fetch(request: Request): Promise<Response>;
nativeFetch(request: Request): Promise<Response>;
send(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: string,
body: any,
authed: boolean,
hasResponse: boolean,
apiUrl?: string | null,
alterHeaders?: (headers: Headers) => void,
): Promise<any>;
postIdentityToken(
request:
| UserApiTokenRequest
| PasswordTokenRequest
| SsoTokenRequest
| WebAuthnLoginTokenRequest,
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
>;
refreshIdentityToken(): Promise<any>;
getActiveBearerToken(): Promise<string>;
}

369
libs/api/src/api.service.ts Normal file
View File

@@ -0,0 +1,369 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request";
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { ClientType, DeviceType } from "@bitwarden/common/enums";
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout/enums/vault-timeout-action.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { flagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { TokenProvider } from "@bitwarden/token-provider";
import { ApiServiceAbstraction } from "./abstractions/api.service";
import { HttpOperations } from "./http-operations";
export class ApiService implements ApiServiceAbstraction {
private device: DeviceType;
private deviceType: string;
private refreshTokenPromise: Promise<string> | undefined;
/**
* The message (responseJson.ErrorModel.Message) that comes back from the server when a new device verification is required.
*/
private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE =
"new device verification required";
constructor(
private tokenService: TokenService,
private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService,
private appIdService: AppIdService,
private refreshAccessTokenErrorCallback: () => void,
private logService: LogService,
private logoutCallback: (logoutReason: LogoutReason) => Promise<void>,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private readonly httpOperations: HttpOperations,
private tokenProvider: TokenProvider,
private customUserAgent: string = null,
) {
this.device = platformUtilsService.getDevice();
this.deviceType = this.device.toString();
}
async postIdentityToken(
request:
| UserApiTokenRequest
| PasswordTokenRequest
| SsoTokenRequest
| WebAuthnLoginTokenRequest,
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
> {
return this.tokenProvider.postIdentityToken(request);
}
async refreshIdentityToken(): Promise<any> {
return this.tokenProvider.refreshIdentityToken();
}
async getActiveBearerToken(): Promise<string> {
return this.tokenProvider.getActiveBearerToken();
}
async fetch(request: Request): Promise<Response> {
if (request.method === "GET") {
request.headers.set("Cache-Control", "no-store");
request.headers.set("Pragma", "no-cache");
}
request.headers.set("Bitwarden-Client-Name", this.platformUtilsService.getClientType());
request.headers.set(
"Bitwarden-Client-Version",
await this.platformUtilsService.getApplicationVersionNumber(),
);
return this.nativeFetch(request);
}
nativeFetch(request: Request): Promise<Response> {
return fetch(request);
}
async send(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: string,
body: any,
authed: boolean,
hasResponse: boolean,
apiUrl?: string | null,
alterHeaders?: (headers: Headers) => void,
): Promise<any> {
const env = await firstValueFrom(this.environmentService.environment$);
apiUrl = Utils.isNullOrWhitespace(apiUrl) ? env.getApiUrl() : apiUrl;
// Prevent directory traversal from malicious paths
const pathParts = path.split("?");
const requestUrl =
apiUrl + Utils.normalizePath(pathParts[0]) + (pathParts.length > 1 ? `?${pathParts[1]}` : "");
const [requestHeaders, requestBody] = await this.buildHeadersAndBody(
authed,
hasResponse,
body,
alterHeaders,
);
const requestInit: RequestInit = {
cache: "no-store",
credentials: await this.getCredentials(),
method: method,
};
requestInit.headers = requestHeaders;
requestInit.body = requestBody;
const response = await this.fetch(this.httpOperations.createRequest(requestUrl, requestInit));
const responseType = response.headers.get("content-type");
const responseIsJson = responseType != null && responseType.indexOf("application/json") !== -1;
const responseIsCsv = responseType != null && responseType.indexOf("text/csv") !== -1;
if (hasResponse && response.status === 200 && responseIsJson) {
const responseJson = await response.json();
return responseJson;
} else if (hasResponse && response.status === 200 && responseIsCsv) {
return await response.text();
} else if (response.status !== 200 && response.status !== 204) {
const error = await this.handleError(response, false, authed);
return Promise.reject(error);
}
}
private async buildHeadersAndBody(
authed: boolean,
hasResponse: boolean,
body: any,
alterHeaders: (headers: Headers) => void,
): Promise<[Headers, any]> {
let requestBody: any = null;
const headers = new Headers({
"Device-Type": this.deviceType,
});
if (flagEnabled("prereleaseBuild")) {
headers.set("Is-Prerelease", "1");
}
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
if (hasResponse) {
headers.set("Accept", "application/json");
}
if (alterHeaders != null) {
alterHeaders(headers);
}
if (authed) {
const authHeader = await this.getActiveBearerToken();
headers.set("Authorization", "Bearer " + authHeader);
} else {
// For unauthenticated requests, we need to tell the server what the device is for flag targeting,
// since it won't be able to get it from the access token.
const appId = await this.appIdService.getAppId();
headers.set("Device-Identifier", appId);
}
if (body != null) {
if (typeof body === "string") {
requestBody = body;
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
} else if (typeof body === "object") {
if (body instanceof FormData) {
requestBody = body;
} else {
headers.set("Content-Type", "application/json; charset=utf-8");
requestBody = JSON.stringify(body);
}
}
}
return [headers, requestBody];
}
private async handleError(
response: Response,
tokenError: boolean,
authed: boolean,
): Promise<ErrorResponse> {
let responseJson: any = null;
if (this.isJsonResponse(response)) {
responseJson = await response.json();
} else if (this.isTextPlainResponse(response)) {
responseJson = { Message: await response.text() };
}
if (authed) {
if (
response.status === 401 ||
response.status === 403 ||
(tokenError &&
response.status === 400 &&
responseJson != null &&
responseJson.error === "invalid_grant")
) {
await this.logoutCallback("invalidGrantError");
}
}
return new ErrorResponse(responseJson, response.status, tokenError);
}
private qsStringify(params: any): string {
return Object.keys(params)
.map((key) => {
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
})
.join("&");
}
private async getCredentials(): Promise<RequestCredentials> {
const env = await firstValueFrom(this.environmentService.environment$);
if (this.platformUtilsService.getClientType() !== ClientType.Web || env.hasBaseUrl()) {
return "include";
}
return undefined;
}
private isJsonResponse(response: Response): boolean {
const typeHeader = response.headers.get("content-type");
return typeHeader != null && typeHeader.indexOf("application/json") > -1;
}
private isTextPlainResponse(response: Response): boolean {
const typeHeader = response.headers.get("content-type");
return typeHeader != null && typeHeader.indexOf("text/plain") > -1;
}
// Token refresh helpers (same behavior as ApiService)
private async internalRefreshToken(): Promise<string> {
const refreshToken = await this.tokenService.getRefreshToken();
if (refreshToken != null && refreshToken !== "") {
return this.refreshAccessToken();
}
const clientId = await this.tokenService.getClientId();
const clientSecret = await this.tokenService.getClientSecret();
if (!Utils.isNullOrWhitespace(clientId) && !Utils.isNullOrWhitespace(clientSecret)) {
return this.refreshApiToken();
}
this.refreshAccessTokenErrorCallback();
throw new Error("Cannot refresh access token, no refresh token or api keys are stored.");
}
protected refreshToken(): Promise<string> {
if (this.refreshTokenPromise === undefined) {
this.refreshTokenPromise = this.internalRefreshToken();
void this.refreshTokenPromise.finally(() => {
this.refreshTokenPromise = undefined;
});
}
return this.refreshTokenPromise;
}
protected async refreshAccessToken(): Promise<string> {
const refreshToken = await this.tokenService.getRefreshToken();
if (refreshToken == null || refreshToken === "") {
throw new Error();
}
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
Accept: "application/json",
"Device-Type": this.deviceType,
});
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
const env = await firstValueFrom(this.environmentService.environment$);
const decodedToken = await this.tokenService.decodeAccessToken();
const response = await this.fetch(
this.httpOperations.createRequest(env.getIdentityUrl() + "/connect/token", {
body: this.qsStringify({
grant_type: "refresh_token",
client_id: decodedToken.client_id,
refresh_token: refreshToken,
}),
cache: "no-store",
credentials: await this.getCredentials(),
headers: headers,
method: "POST",
}),
);
if (response.status === 200) {
const responseJson = await response.json();
const tokenResponse = new IdentityTokenResponse(responseJson);
const newDecodedAccessToken = await this.tokenService.decodeAccessToken(
tokenResponse.accessToken,
);
const userId = newDecodedAccessToken.sub;
const vaultTimeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
const vaultTimeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
const refreshedTokens = await this.tokenService.setTokens(
tokenResponse.accessToken,
vaultTimeoutAction as VaultTimeoutAction,
vaultTimeout,
tokenResponse.refreshToken,
);
return refreshedTokens.accessToken;
} else {
const error = await this.handleError(response, true, true);
return Promise.reject(error);
}
}
protected async refreshApiToken(): Promise<string> {
const clientId = await this.tokenService.getClientId();
const clientSecret = await this.tokenService.getClientSecret();
const appId = await this.appIdService.getAppId();
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
const tokenRequest = new UserApiTokenRequest(
clientId,
clientSecret,
new TokenTwoFactorRequest(),
deviceRequest,
);
const response = await this.postIdentityToken(tokenRequest);
if (!(response instanceof IdentityTokenResponse)) {
throw new Error("Invalid response received when refreshing api token");
}
const newDecodedAccessToken = await this.tokenService.decodeAccessToken(response.accessToken);
const userId = newDecodedAccessToken.sub;
const vaultTimeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
const vaultTimeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
const refreshedToken = await this.tokenService.setAccessToken(
response.accessToken,
vaultTimeoutAction as VaultTimeoutAction,
vaultTimeout,
);
return refreshedToken;
}
}

8
libs/api/src/api.spec.ts Normal file
View File

@@ -0,0 +1,8 @@
import * as lib from "./index";
describe("api", () => {
// This test will fail until something is exported from index.ts
it("should work", () => {
expect(lib).toBeDefined();
});
});

View File

@@ -0,0 +1,3 @@
export type HttpOperations = {
createRequest: (url: string, request: RequestInit) => Request;
};

3
libs/api/src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from "./api.service";
export * from "./api.service.abstraction";
export * from "./http-operations";

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["**/build", "**/dist"]
}

13
libs/api/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@@ -0,0 +1,5 @@
# legacy-api
Owned by: platform
Legacy API implementation for back-compat during extraction from @bitwarden/common

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

View File

@@ -0,0 +1,10 @@
module.exports = {
displayName: "legacy-api",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/legacy-api",
};

View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/legacy-api",
"version": "0.0.1",
"description": "Legacy API implementation for back-compat during extraction from @bitwarden/common",
"private": true,
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "GPL-3.0",
"author": "platform"
}

View File

@@ -0,0 +1,33 @@
{
"name": "legacy-api",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/legacy-api/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/legacy-api",
"main": "libs/legacy-api/src/index.ts",
"tsConfig": "libs/legacy-api/tsconfig.lib.json",
"assets": ["libs/legacy-api/*.md"]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/legacy-api/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/legacy-api/jest.config.js"
}
}
}
}

View File

View File

@@ -0,0 +1,8 @@
import * as lib from "./index";
describe("legacy-api", () => {
// This test will fail until something is exported from index.ts
it("should work", () => {
expect(lib).toBeDefined();
});
});

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["**/build", "**/dist"]
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

18
package-lock.json generated
View File

@@ -308,6 +308,11 @@
"version": "0.0.0",
"license": "GPL-3.0"
},
"libs/api": {
"name": "@bitwarden/api",
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/auth": {
"name": "@bitwarden/auth",
"version": "0.0.0",
@@ -372,6 +377,11 @@
"version": "0.0.0",
"license": "GPL-3.0"
},
"libs/legacy-api": {
"name": "@bitwarden/legacy-api",
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/logging": {
"name": "@bitwarden/logging",
"version": "0.0.1",
@@ -4579,6 +4589,10 @@
"resolved": "libs/angular",
"link": true
},
"node_modules/@bitwarden/api": {
"resolved": "libs/api",
"link": true
},
"node_modules/@bitwarden/auth": {
"resolved": "libs/auth",
"link": true
@@ -4667,6 +4681,10 @@
"resolved": "libs/key-management-ui",
"link": true
},
"node_modules/@bitwarden/legacy-api": {
"resolved": "libs/legacy-api",
"link": true
},
"node_modules/@bitwarden/logging": {
"resolved": "libs/logging",
"link": true

View File

@@ -20,6 +20,7 @@
"paths": {
"@bitwarden/admin-console/common": ["./libs/admin-console/src/common"],
"@bitwarden/angular/*": ["./libs/angular/src/*"],
"@bitwarden/api": ["libs/api/src/index.ts"],
"@bitwarden/auth/angular": ["./libs/auth/src/angular"],
"@bitwarden/auth/common": ["./libs/auth/src/common"],
"@bitwarden/billing": ["./libs/billing/src"],
@@ -42,6 +43,7 @@
"@bitwarden/importer-ui": ["./libs/importer/src/components"],
"@bitwarden/key-management": ["./libs/key-management/src"],
"@bitwarden/key-management-ui": ["./libs/key-management-ui/src"],
"@bitwarden/legacy-api": ["libs/legacy-api/src/index.ts"],
"@bitwarden/logging": ["libs/logging/src"],
"@bitwarden/messaging": ["libs/messaging/src/index.ts"],
"@bitwarden/messaging-internal": ["libs/messaging-internal/src/index.ts"],