1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 17:43:22 +00:00

[PM-31763] Add unlock service & module (#18870)

* Add unlock service

* Move methods

* Prettier

* Fix type errors

* Prettier

* Fix test

* Fix module order

* Attempt to fix tests

* Cleanup CODEOWNERS

* Backport biometric unlock and legacy master-key logic

* Add tests for biometrics

* Prettier

* Add biometric unlock to abstract unlock service

* Fix build
This commit is contained in:
Bernd Schoolmann
2026-02-25 18:50:25 +01:00
committed by GitHub
parent 4f706746d6
commit 666ff42d22
20 changed files with 631 additions and 5 deletions

3
.github/CODEOWNERS vendored
View File

@@ -92,6 +92,7 @@ libs/angular/src/billing @bitwarden/team-billing-dev
libs/common/src/billing @bitwarden/team-billing-dev
libs/billing @bitwarden/team-billing-dev
libs/pricing @bitwarden/team-billing-dev
libs/subscription @bitwarden/team-billing-dev
bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev
## Platform team files ##
@@ -191,6 +192,7 @@ libs/key-management @bitwarden/team-key-management-dev
libs/key-management-ui @bitwarden/team-key-management-dev
libs/user-crypto-management @bitwarden/team-key-management-dev
libs/common/src/key-management @bitwarden/team-key-management-dev
libs/unlock @bitwarden/team-key-management-dev
# Node-cryptofunction service
libs/node @bitwarden/team-key-management-dev
@@ -247,4 +249,3 @@ apps/desktop/native-messaging-test-runner/package-lock.json @bitwarden/team-plat
.claude/ @bitwarden/team-ai-sme
.github/workflows/respond.yml @bitwarden/team-ai-sme
.github/workflows/review-code.yml @bitwarden/team-ai-sme
libs/subscription @bitwarden/team-billing-dev

View File

@@ -61,6 +61,7 @@ module.exports = {
"<rootDir>/libs/vault/jest.config.js",
"<rootDir>/libs/auto-confirm/jest.config.js",
"<rootDir>/libs/subscription/jest.config.js",
"<rootDir>/libs/unlock/jest.config.js",
"<rootDir>/libs/user-crypto-management/jest.config.js",
],

View File

@@ -389,6 +389,7 @@ import {
DefaultStateService,
} from "@bitwarden/state-internal";
import { SafeInjectionToken } from "@bitwarden/ui-common";
import { DefaultUnlockService, UnlockService } from "@bitwarden/unlock";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { PasswordRepromptService } from "@bitwarden/vault";
@@ -919,6 +920,22 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultAccountCryptographicStateService,
deps: [StateProvider],
}),
safeProvider({
provide: UnlockService,
useClass: DefaultUnlockService,
deps: [
RegisterSdkService,
AccountCryptographicStateService,
PinStateServiceAbstraction,
KdfConfigService,
AccountServiceAbstraction,
InternalMasterPasswordServiceAbstraction,
CryptoFunctionServiceAbstraction,
StateProvider,
LogService,
BiometricsService,
],
}),
safeProvider({
provide: BroadcasterService,
useClass: DefaultBroadcasterService,

View File

@@ -45,10 +45,14 @@ export const MASTER_KEY = new UserKeyDefinition<MasterKey>(MASTER_PASSWORD_MEMOR
});
/** Disk since master key hash is used for unlock */
const MASTER_KEY_HASH = new UserKeyDefinition<string>(MASTER_PASSWORD_DISK, "masterKeyHash", {
deserializer: (masterKeyHash) => masterKeyHash,
clearOn: ["logout"],
});
export const MASTER_KEY_HASH = new UserKeyDefinition<string>(
MASTER_PASSWORD_DISK,
"masterKeyHash",
{
deserializer: (masterKeyHash) => masterKeyHash,
clearOn: ["logout"],
},
);
/** Disk to persist through lock */
export const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition<EncryptedString>(

View File

@@ -45,6 +45,14 @@ export class MasterPasswordUnlockData {
);
}
toSdk(): SdkMasterPasswordUnlockData {
return {
salt: this.salt,
kdf: this.kdf.toSdkConfig(),
masterKeyWrappedUserKey: this.masterKeyWrappedUserKey,
};
}
toJSON(): any {
return {
salt: this.salt,

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

@@ -0,0 +1,5 @@
# Unlock
Owned by: key-management
Unlock the account of a user using any of their unlock methods.

View File

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

View File

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

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

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/unlock",
"version": "0.0.1",
"description": "Unlock the account of a user",
"private": true,
"type": "commonjs",
"main": "index.js",
"types": "index.d.ts",
"license": "GPL-3.0",
"author": "key-management"
}

34
libs/unlock/project.json Normal file
View File

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

View File

@@ -0,0 +1,219 @@
// Polyfill for Symbol.dispose required by the service's use of `using` keyword
import "core-js/proposals/explicit-resource-management";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinStateServiceAbstraction } from "@bitwarden/common/key-management/pin/pin-state.service.abstraction";
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key";
import { BiometricsService, KdfConfigService } from "@bitwarden/key-management";
import { LogService } from "@bitwarden/logging";
import { PureCrypto } from "@bitwarden/sdk-internal";
import { StateProvider } from "@bitwarden/state";
import { DefaultUnlockService } from "./default-unlock.service";
const mockUserId = "b1e2d3c4-a1b2-c3d4-e5f6-a1b2c3d4e5f6" as UserId;
const mockEmail = "test@example.com";
const mockPin = "1234";
const mockMasterPassword = "master-password";
const mockKdfParams = { type: "pbkdf2" } as any;
const mockAccountCryptographicState = { some: "state" } as any;
const mockPinProtectedUserKeyEnvelope = { some: "envelope" } as any;
const mockMasterPasswordUnlockData = { some: "unlockData", salt: "salt", kdf: "pbkdf2" } as any;
describe("DefaultUnlockService", () => {
const registerSdkService = mock<RegisterSdkService>();
const accountCryptographicStateService = mock<AccountCryptographicStateService>();
const pinStateService = mock<PinStateServiceAbstraction>();
const kdfService = mock<KdfConfigService>();
const accountService = mock<AccountService>();
const masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
const cryptoFunctionService = mock<CryptoFunctionService>();
const stateProvider = mock<StateProvider>();
const logService = mock<LogService>();
const biometricsService = mock<BiometricsService>();
let service: DefaultUnlockService;
let mockSdkRef: any;
let mockSdk: any;
let mockCrypto: any;
beforeEach(() => {
jest.resetAllMocks();
mockCrypto = {
initialize_user_crypto: jest.fn().mockResolvedValue(undefined),
};
mockSdkRef = {
value: {
crypto: jest.fn().mockReturnValue(mockCrypto),
},
[Symbol.dispose]: jest.fn(),
};
mockSdk = {
take: jest.fn().mockReturnValue(mockSdkRef),
};
registerSdkService.registerClient$.mockReturnValue(of(mockSdk));
accountCryptographicStateService.accountCryptographicState$.mockReturnValue(
of(mockAccountCryptographicState),
);
kdfService.getKdfConfig$.mockReturnValue(of({ toSdkConfig: () => mockKdfParams } as any));
accountService.accounts$ = of({
[mockUserId]: { email: mockEmail },
} as any);
pinStateService.getPinLockType.mockResolvedValue("PERSISTENT" as any);
pinStateService.getPinProtectedUserKeyEnvelope.mockResolvedValue(
mockPinProtectedUserKeyEnvelope,
);
masterPasswordService.masterPasswordUnlockData$.mockReturnValue(
of({ toSdk: () => mockMasterPasswordUnlockData } as any),
);
Object.defineProperty(SdkLoadService, "Ready", {
value: Promise.resolve(),
writable: true,
configurable: true,
});
jest.spyOn(PureCrypto, "derive_kdf_material").mockReturnValue(new Uint8Array(32));
cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32));
const mockStateUpdate = jest.fn().mockResolvedValue(undefined);
stateProvider.getUser.mockReturnValue({ update: mockStateUpdate } as any);
service = new DefaultUnlockService(
registerSdkService,
accountCryptographicStateService,
pinStateService,
kdfService,
accountService,
masterPasswordService,
cryptoFunctionService,
stateProvider,
logService,
biometricsService,
);
});
describe("unlockWithPin", () => {
it("calls SDK initialize_user_crypto with correct pin method", async () => {
await service.unlockWithPin(mockUserId, mockPin);
expect(mockCrypto.initialize_user_crypto).toHaveBeenCalledWith({
userId: mockUserId,
kdfParams: mockKdfParams,
email: mockEmail,
accountCryptographicState: mockAccountCryptographicState,
method: {
pinEnvelope: {
pin: mockPin,
pin_protected_user_key_envelope: mockPinProtectedUserKeyEnvelope,
},
},
});
});
it("throws when SDK is not available", async () => {
registerSdkService.registerClient$.mockReturnValue(of(null as any));
await expect(service.unlockWithPin(mockUserId, mockPin)).rejects.toThrow("SDK not available");
});
it("fetches PERSISTENT pin envelope when the pin lock type is persistent", async () => {
pinStateService.getPinLockType.mockResolvedValue("PERSISTENT" as any);
await service.unlockWithPin(mockUserId, mockPin);
expect(pinStateService.getPinProtectedUserKeyEnvelope).toHaveBeenCalledWith(
mockUserId,
"PERSISTENT",
);
});
it("fetches EPHEMERAL pin envelope when the pin lock type is ephemeral", async () => {
pinStateService.getPinLockType.mockResolvedValue("EPHEMERAL" as any);
await service.unlockWithPin(mockUserId, mockPin);
expect(pinStateService.getPinProtectedUserKeyEnvelope).toHaveBeenCalledWith(
mockUserId,
"EPHEMERAL",
);
});
});
describe("unlockWithMasterPassword", () => {
it("calls SDK initialize_user_crypto with correct master password method", async () => {
await service.unlockWithMasterPassword(mockUserId, mockMasterPassword);
expect(mockCrypto.initialize_user_crypto).toHaveBeenCalledWith({
userId: mockUserId,
kdfParams: mockKdfParams,
email: mockEmail,
accountCryptographicState: mockAccountCryptographicState,
method: {
masterPasswordUnlock: {
password: mockMasterPassword,
master_password_unlock: mockMasterPasswordUnlockData,
},
},
});
});
it("throws when SDK is not available", async () => {
registerSdkService.registerClient$.mockReturnValue(of(null as any));
await expect(
service.unlockWithMasterPassword(mockUserId, mockMasterPassword),
).rejects.toThrow("SDK not available");
});
});
describe("unlockWithBiometrics", () => {
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey;
it("calls SDK initialize_user_crypto with decrypted key from biometrics", async () => {
biometricsService.unlockWithBiometricsForUser.mockResolvedValue(mockUserKey);
await service.unlockWithBiometrics(mockUserId);
expect(biometricsService.unlockWithBiometricsForUser).toHaveBeenCalledWith(mockUserId);
expect(mockCrypto.initialize_user_crypto).toHaveBeenCalledWith({
userId: mockUserId,
kdfParams: mockKdfParams,
email: mockEmail,
accountCryptographicState: mockAccountCryptographicState,
method: {
decryptedKey: {
decrypted_user_key: mockUserKey.toBase64(),
},
},
});
});
it("throws when biometrics returns null", async () => {
biometricsService.unlockWithBiometricsForUser.mockResolvedValue(null);
await expect(service.unlockWithBiometrics(mockUserId)).rejects.toThrow(
"Failed to unlock with biometrics",
);
});
it("throws when SDK is not available", async () => {
biometricsService.unlockWithBiometricsForUser.mockResolvedValue(mockUserKey);
registerSdkService.registerClient$.mockReturnValue(of(null as any));
await expect(service.unlockWithBiometrics(mockUserId)).rejects.toThrow("SDK not available");
});
});
});

View File

@@ -0,0 +1,227 @@
import { firstValueFrom, map } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { assertNonNullish } from "@bitwarden/common/auth/utils";
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import {
MASTER_KEY,
MASTER_KEY_HASH,
} from "@bitwarden/common/key-management/master-password/services/master-password.service";
import { PinStateServiceAbstraction } from "@bitwarden/common/key-management/pin/pin-state.service.abstraction";
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { MasterKey } from "@bitwarden/common/types/key";
import { BiometricsService, KdfConfig, KdfConfigService } from "@bitwarden/key-management";
import { LogService } from "@bitwarden/logging";
import {
Kdf,
MasterPasswordUnlockData,
PasswordProtectedKeyEnvelope,
PureCrypto,
WrappedAccountCryptographicState,
} from "@bitwarden/sdk-internal";
import { StateProvider } from "@bitwarden/state";
import { UserId } from "@bitwarden/user-core";
import { UnlockService } from "./unlock.service";
export class DefaultUnlockService implements UnlockService {
constructor(
private registerSdkService: RegisterSdkService,
private accountCryptographicStateService: AccountCryptographicStateService,
private pinStateService: PinStateServiceAbstraction,
private kdfService: KdfConfigService,
private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private cryptoFunctionService: CryptoFunctionService,
private stateProvider: StateProvider,
private logService: LogService,
private biometricsService: BiometricsService,
) {}
async unlockWithPin(userId: UserId, pin: string): Promise<void> {
const startTime = performance.now();
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId)!,
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
pinEnvelope: {
pin: pin,
pin_protected_user_key_envelope: await this.getPinProtectedUserKeyEnvelope(userId),
},
},
});
}),
),
);
this.logService.measure(startTime, "Unlock", "DefaultUnlockService", "unlockWithPin");
}
async unlockWithMasterPassword(userId: UserId, masterPassword: string): Promise<void> {
const startTime = performance.now();
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId),
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
masterPasswordUnlock: {
password: masterPassword,
master_password_unlock: await this.getMasterPasswordUnlockData(userId),
},
},
});
}),
),
);
await this.setLegacyMasterKeyFromUnlockData(
masterPassword,
await this.getMasterPasswordUnlockData(userId),
userId,
);
this.logService.measure(
startTime,
"Unlock",
"DefaultUnlockService",
"unlockWithMasterPassword",
);
}
async unlockWithBiometrics(userId: UserId): Promise<void> {
// First, get the biometrics-protected user key. This will prompt the user to authenticate with biometrics.
const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId);
if (!userKey) {
throw new Error("Failed to unlock with biometrics");
}
// Now that we have the biometrics-protected user key, we can initialize the SDK with it to complete the unlock process.
const startTime = performance.now();
await firstValueFrom(
this.registerSdkService.registerClient$(userId).pipe(
map(async (sdk) => {
if (!sdk) {
throw new Error("SDK not available");
}
using ref = sdk.take();
return ref.value.crypto().initialize_user_crypto({
userId: asUuid(userId),
kdfParams: await this.getKdfParams(userId),
email: await this.getEmail(userId),
accountCryptographicState: await this.getAccountCryptographicState(userId),
method: {
decryptedKey: {
decrypted_user_key: userKey.toBase64(),
},
},
});
}),
),
);
this.logService.measure(startTime, "Unlock", "DefaultUnlockService", "unlockWithBiometrics");
}
private async getAccountCryptographicState(
userId: UserId,
): Promise<WrappedAccountCryptographicState> {
const accountCryptographicState = await firstValueFrom(
this.accountCryptographicStateService.accountCryptographicState$(userId),
);
assertNonNullish(accountCryptographicState, "Account cryptographic state is required");
return accountCryptographicState!;
}
private async getKdfParams(userId: UserId): Promise<Kdf> {
const kdfParams = await firstValueFrom(
this.kdfService.getKdfConfig$(userId).pipe(
map((config: KdfConfig | null) => {
return config?.toSdkConfig();
}),
),
);
assertNonNullish(kdfParams, "KDF parameters are required");
return kdfParams!;
}
private async getEmail(userId: UserId): Promise<string> {
const accounts = await firstValueFrom(this.accountService.accounts$);
const email = accounts[userId].email;
assertNonNullish(email, "Email is required");
return email;
}
private async getPinProtectedUserKeyEnvelope(
userId: UserId,
): Promise<PasswordProtectedKeyEnvelope> {
const pinLockType = await this.pinStateService.getPinLockType(userId);
const pinEnvelope = await this.pinStateService.getPinProtectedUserKeyEnvelope(
userId,
pinLockType,
);
assertNonNullish(pinEnvelope, "User is not enrolled in PIN unlock");
return pinEnvelope!;
}
private async getMasterPasswordUnlockData(userId: UserId): Promise<MasterPasswordUnlockData> {
const unlockData = await firstValueFrom(
this.masterPasswordService.masterPasswordUnlockData$(userId),
);
assertNonNullish(unlockData, "Master password unlock data is required");
return unlockData.toSdk();
}
private async setLegacyMasterKeyFromUnlockData(
password: string,
masterPasswordUnlockData: MasterPasswordUnlockData,
userId: UserId,
): Promise<void> {
assertNonNullish(password, "password");
assertNonNullish(masterPasswordUnlockData, "masterPasswordUnlockData");
assertNonNullish(userId, "userId");
this.logService.info("[DefaultUnlockService] Setting legacy master key from unlock data");
// NOTE: This entire section is deprecated and will be removed as soon as
// the masterkey is dropped from state. It is very temporary.
await SdkLoadService.Ready;
const passwordBuffer = new TextEncoder().encode(password);
const saltBuffer = new TextEncoder().encode(masterPasswordUnlockData.salt);
const masterKey = PureCrypto.derive_kdf_material(
passwordBuffer,
saltBuffer,
masterPasswordUnlockData.kdf,
);
const hash = await this.cryptoFunctionService.pbkdf2(
masterKey,
password,
"sha256",
2, // HashPurpose.LocalAuthorization
);
await this.stateProvider
.getUser(userId, MASTER_KEY)
.update((_) => new SymmetricCryptoKey(masterKey) as MasterKey);
await this.stateProvider
.getUser(userId, MASTER_KEY_HASH)
.update((_) => Utils.fromBufferToB64(hash));
}
}

2
libs/unlock/src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { UnlockService } from "./unlock.service";
export { DefaultUnlockService } from "./default-unlock.service";

View File

@@ -0,0 +1,35 @@
import { UserId } from "@bitwarden/common/types/guid";
/**
* Service for unlocking a user's account with various methods.
*/
export abstract class UnlockService {
/**
* Unlocks the user's account using their PIN.
*
* @param userId - The user's id
* @param pin - The user's PIN
* @throws If the SDK is not available
* @throws If the PIN is invalid or decryption fails
*/
abstract unlockWithPin(userId: UserId, pin: string): Promise<void>;
/**
* Unlocks the user's account using their master password.
*
* @param userId - The user's id
* @param masterPassword - The user's master password
* @throws If the SDK is not available
* @throws If the master password is invalid or decryption fails
*/
abstract unlockWithMasterPassword(userId: UserId, masterPassword: string): Promise<void>;
/**
* Unlocks the user's account using biometrics.
*
* @param userId - The user's id
* @throws If the SDK is not available
* @throws If biometric authentication fails
*/
abstract unlockWithBiometrics(userId: UserId): Promise<void>;
}

View File

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

13
libs/unlock/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"]
}

9
package-lock.json generated
View File

@@ -637,6 +637,11 @@
"version": "0.0.0",
"license": "GPL-3.0"
},
"libs/unlock": {
"name": "@bitwarden/unlock",
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/user-core": {
"name": "@bitwarden/user-core",
"version": "0.0.0",
@@ -5108,6 +5113,10 @@
"resolved": "libs/ui/common",
"link": true
},
"node_modules/@bitwarden/unlock": {
"resolved": "libs/unlock",
"link": true
},
"node_modules/@bitwarden/user-core": {
"resolved": "libs/user-core",
"link": true

View File

@@ -64,6 +64,7 @@
"@bitwarden/subscription": ["./libs/subscription/src/index.ts"],
"@bitwarden/ui-common": ["./libs/ui/common/src"],
"@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"],
"@bitwarden/unlock": ["./libs/unlock/src/index.ts"],
"@bitwarden/user-core": ["./libs/user-core/src/index.ts"],
"@bitwarden/user-crypto-management": ["./libs/user-crypto-management/src/index.ts"],
"@bitwarden/vault": ["./libs/vault/src"],