From d509d277eea29e12864b144def48206260f7486d Mon Sep 17 00:00:00 2001 From: addisonbeck Date: Fri, 8 Aug 2025 15:45:19 -0400 Subject: [PATCH] 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 --- .github/CODEOWNERS | 2 + libs/api/README.md | 5 + libs/api/eslint.config.mjs | 3 + libs/api/jest.config.js | 10 + libs/api/package.json | 11 + libs/api/project.json | 33 +++ libs/api/src/api.service.abstraction.ts | 33 +++ libs/api/src/api.service.ts | 369 ++++++++++++++++++++++++ libs/api/src/api.spec.ts | 8 + libs/api/src/http-operations.ts | 3 + libs/api/src/index.ts | 3 + libs/api/tsconfig.eslint.json | 6 + libs/api/tsconfig.json | 13 + libs/api/tsconfig.lib.json | 10 + libs/api/tsconfig.spec.json | 10 + libs/legacy-api/README.md | 5 + libs/legacy-api/eslint.config.mjs | 3 + libs/legacy-api/jest.config.js | 10 + libs/legacy-api/package.json | 11 + libs/legacy-api/project.json | 33 +++ libs/legacy-api/src/index.ts | 0 libs/legacy-api/src/legacy-api.spec.ts | 8 + libs/legacy-api/tsconfig.eslint.json | 6 + libs/legacy-api/tsconfig.json | 13 + libs/legacy-api/tsconfig.lib.json | 10 + libs/legacy-api/tsconfig.spec.json | 10 + package-lock.json | 18 ++ tsconfig.base.json | 2 + 28 files changed, 648 insertions(+) create mode 100644 libs/api/README.md create mode 100644 libs/api/eslint.config.mjs create mode 100644 libs/api/jest.config.js create mode 100644 libs/api/package.json create mode 100644 libs/api/project.json create mode 100644 libs/api/src/api.service.abstraction.ts create mode 100644 libs/api/src/api.service.ts create mode 100644 libs/api/src/api.spec.ts create mode 100644 libs/api/src/http-operations.ts create mode 100644 libs/api/src/index.ts create mode 100644 libs/api/tsconfig.eslint.json create mode 100644 libs/api/tsconfig.json create mode 100644 libs/api/tsconfig.lib.json create mode 100644 libs/api/tsconfig.spec.json create mode 100644 libs/legacy-api/README.md create mode 100644 libs/legacy-api/eslint.config.mjs create mode 100644 libs/legacy-api/jest.config.js create mode 100644 libs/legacy-api/package.json create mode 100644 libs/legacy-api/project.json create mode 100644 libs/legacy-api/src/index.ts create mode 100644 libs/legacy-api/src/legacy-api.spec.ts create mode 100644 libs/legacy-api/tsconfig.eslint.json create mode 100644 libs/legacy-api/tsconfig.json create mode 100644 libs/legacy-api/tsconfig.lib.json create mode 100644 libs/legacy-api/tsconfig.spec.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8340c89ab55..3d5176855c2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/libs/api/README.md b/libs/api/README.md new file mode 100644 index 00000000000..1e0c2e12e07 --- /dev/null +++ b/libs/api/README.md @@ -0,0 +1,5 @@ +# api + +Owned by: platform + +Library for Bitwarden API access diff --git a/libs/api/eslint.config.mjs b/libs/api/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/api/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/api/jest.config.js b/libs/api/jest.config.js new file mode 100644 index 00000000000..d8dbc16e786 --- /dev/null +++ b/libs/api/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "api", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/api", +}; diff --git a/libs/api/package.json b/libs/api/package.json new file mode 100644 index 00000000000..95969bf9365 --- /dev/null +++ b/libs/api/package.json @@ -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" +} diff --git a/libs/api/project.json b/libs/api/project.json new file mode 100644 index 00000000000..20e904d0fbd --- /dev/null +++ b/libs/api/project.json @@ -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" + } + } + } +} diff --git a/libs/api/src/api.service.abstraction.ts b/libs/api/src/api.service.abstraction.ts new file mode 100644 index 00000000000..c2d9b938045 --- /dev/null +++ b/libs/api/src/api.service.abstraction.ts @@ -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; + nativeFetch(request: Request): Promise; + send( + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH", + path: string, + body: any, + authed: boolean, + hasResponse: boolean, + apiUrl?: string | null, + alterHeaders?: (headers: Headers) => void, + ): Promise; + + postIdentityToken( + request: + | UserApiTokenRequest + | PasswordTokenRequest + | SsoTokenRequest + | WebAuthnLoginTokenRequest, + ): Promise< + IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse + >; + refreshIdentityToken(): Promise; + getActiveBearerToken(): Promise; +} diff --git a/libs/api/src/api.service.ts b/libs/api/src/api.service.ts new file mode 100644 index 00000000000..30acd62c845 --- /dev/null +++ b/libs/api/src/api.service.ts @@ -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 | 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, + 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 { + return this.tokenProvider.refreshIdentityToken(); + } + + async getActiveBearerToken(): Promise { + return this.tokenProvider.getActiveBearerToken(); + } + + async fetch(request: Request): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + if (this.refreshTokenPromise === undefined) { + this.refreshTokenPromise = this.internalRefreshToken(); + void this.refreshTokenPromise.finally(() => { + this.refreshTokenPromise = undefined; + }); + } + return this.refreshTokenPromise; + } + + protected async refreshAccessToken(): Promise { + 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 { + 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; + } +} diff --git a/libs/api/src/api.spec.ts b/libs/api/src/api.spec.ts new file mode 100644 index 00000000000..3c70f580437 --- /dev/null +++ b/libs/api/src/api.spec.ts @@ -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(); + }); +}); diff --git a/libs/api/src/http-operations.ts b/libs/api/src/http-operations.ts new file mode 100644 index 00000000000..c4bd82501cb --- /dev/null +++ b/libs/api/src/http-operations.ts @@ -0,0 +1,3 @@ +export type HttpOperations = { + createRequest: (url: string, request: RequestInit) => Request; +}; diff --git a/libs/api/src/index.ts b/libs/api/src/index.ts new file mode 100644 index 00000000000..481f0807a1f --- /dev/null +++ b/libs/api/src/index.ts @@ -0,0 +1,3 @@ +export * from "./api.service"; +export * from "./api.service.abstraction"; +export * from "./http-operations"; diff --git a/libs/api/tsconfig.eslint.json b/libs/api/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/api/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/api/tsconfig.json b/libs/api/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/api/tsconfig.lib.json b/libs/api/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/api/tsconfig.lib.json @@ -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"] +} diff --git a/libs/api/tsconfig.spec.json b/libs/api/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/api/tsconfig.spec.json @@ -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"] +} diff --git a/libs/legacy-api/README.md b/libs/legacy-api/README.md new file mode 100644 index 00000000000..670bb431095 --- /dev/null +++ b/libs/legacy-api/README.md @@ -0,0 +1,5 @@ +# legacy-api + +Owned by: platform + +Legacy API implementation for back-compat during extraction from @bitwarden/common diff --git a/libs/legacy-api/eslint.config.mjs b/libs/legacy-api/eslint.config.mjs new file mode 100644 index 00000000000..9c37d10e3ff --- /dev/null +++ b/libs/legacy-api/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from "../../eslint.config.mjs"; + +export default [...baseConfig]; diff --git a/libs/legacy-api/jest.config.js b/libs/legacy-api/jest.config.js new file mode 100644 index 00000000000..e3ec121717f --- /dev/null +++ b/libs/legacy-api/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: "legacy-api", + preset: "../../jest.preset.js", + testEnvironment: "node", + transform: { + "^.+\\.[tj]s$": ["ts-jest", { tsconfig: "/tsconfig.spec.json" }], + }, + moduleFileExtensions: ["ts", "js", "html"], + coverageDirectory: "../../coverage/libs/legacy-api", +}; diff --git a/libs/legacy-api/package.json b/libs/legacy-api/package.json new file mode 100644 index 00000000000..7102abf5ff0 --- /dev/null +++ b/libs/legacy-api/package.json @@ -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" +} diff --git a/libs/legacy-api/project.json b/libs/legacy-api/project.json new file mode 100644 index 00000000000..6bef9645409 --- /dev/null +++ b/libs/legacy-api/project.json @@ -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" + } + } + } +} diff --git a/libs/legacy-api/src/index.ts b/libs/legacy-api/src/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/legacy-api/src/legacy-api.spec.ts b/libs/legacy-api/src/legacy-api.spec.ts new file mode 100644 index 00000000000..3c50141d955 --- /dev/null +++ b/libs/legacy-api/src/legacy-api.spec.ts @@ -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(); + }); +}); diff --git a/libs/legacy-api/tsconfig.eslint.json b/libs/legacy-api/tsconfig.eslint.json new file mode 100644 index 00000000000..3daf120441a --- /dev/null +++ b/libs/legacy-api/tsconfig.eslint.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": ["**/build", "**/dist"] +} diff --git a/libs/legacy-api/tsconfig.json b/libs/legacy-api/tsconfig.json new file mode 100644 index 00000000000..62ebbd94647 --- /dev/null +++ b/libs/legacy-api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/legacy-api/tsconfig.lib.json b/libs/legacy-api/tsconfig.lib.json new file mode 100644 index 00000000000..9cbf6736007 --- /dev/null +++ b/libs/legacy-api/tsconfig.lib.json @@ -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"] +} diff --git a/libs/legacy-api/tsconfig.spec.json b/libs/legacy-api/tsconfig.spec.json new file mode 100644 index 00000000000..1275f148a18 --- /dev/null +++ b/libs/legacy-api/tsconfig.spec.json @@ -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"] +} diff --git a/package-lock.json b/package-lock.json index 00ffd6e2297..51ceeae2735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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 diff --git a/tsconfig.base.json b/tsconfig.base.json index d1b4c1ed642..dbd11f82f46 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"],