1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-30 16:23:53 +00:00

generate CLAUDE.md:

This commit is contained in:
addisonbeck
2026-01-14 14:57:43 -05:00
parent 935af33725
commit bc63221ff1

874
CLAUDE.md Normal file
View File

@@ -0,0 +1,874 @@
# Bitwarden Client Applications - Claude Code Configuration
Bitwarden client applications monorepo containing all non-mobile clients: web vault, browser extension, desktop app (Electron), and CLI. An open-source password manager focused on security, privacy, and cross-platform accessibility.
## Overview
### What This Project Does
- **Password Management**: Secure storage and synchronization of passwords, notes, cards, identities, and SSH keys across all platforms
- **Key Entry Points**:
- Web Vault: `apps/web/src/main.ts` (Angular SPA)
- Browser Extension: `apps/browser/src/popup/main.ts` (popup), `apps/browser/src/platform/background.ts` (service worker)
- Desktop: `apps/desktop/src/main.ts` (Electron main process)
- CLI: `apps/cli/src/bw.ts` (Node.js command-line tool)
- **Target Users**: Individual users, teams, and enterprise organizations requiring secure credential management
### Key Concepts
- **Cipher**: The core encrypted vault item (login, card, identity, secure note, SSH key)
- **Collection**: Organizational grouping for sharing ciphers with team members
- **Folder**: Personal organizational structure for ciphers (user-only, not shared)
- **Organization**: Enterprise/team account that manages users, collections, and policies
- **Master Password**: Primary authentication credential used to derive encryption keys
- **User Key**: Symmetric key that encrypts all user vault data, itself encrypted by the master key
- **Trusted Device Encryption (TDE)**: Allows device-based vault decryption without master password
- **EncString**: Encrypted string type used throughout for all sensitive data storage
---
## Architecture & Patterns
### System Architecture
```
User Request
|
┌────┴────┐
│ Client │ (Browser/Desktop/CLI/Web)
└────┬────┘
┌────┴─────────────────────────────────────────┐
│ @bitwarden/angular │
│ (Shared Angular Components) │
└────┬─────────────────────────────────────────┘
┌────┴─────────────────────────────────────────┐
│ @bitwarden/auth │
│ (Login Strategies, Guards, 2FA, SSO) │
└────┬─────────────────────────────────────────┘
┌────┴─────────────────────────────────────────┐
│ @bitwarden/common │
│ (Platform-agnostic: Models, Services, Crypto)│
└────┬─────────────────────────────────────────┘
┌────┴─────────────────────────────────────────┐
│ @bitwarden/key-management │
│ (Cryptographic Key Operations) │
└────┬─────────────────────────────────────────┘
┌────┴─────────────────────────────────────────┐
│ @bitwarden/state │
│ (Reactive State Management) │
└────┴─────────────────────────────────────────┘
Bitwarden API Server
```
### Code Organization
```
bitwarden-clients/
├── apps/
│ ├── browser/ # Chrome/Firefox/Safari/Edge extension
│ │ ├── src/popup/ # Extension popup UI
│ │ ├── src/background/ # Service worker
│ │ └── src/platform/ # BrowserApi abstraction
│ ├── cli/ # Command-line interface
│ │ ├── src/commands/ # CLI command implementations
│ │ └── src/service-container/ # DI setup
│ ├── desktop/ # Electron desktop app
│ │ ├── src/main/ # Electron main process
│ │ └── src/ # Renderer process (Angular)
│ └── web/ # Web vault (Angular SPA)
│ ├── src/app/ # Angular application
│ └── config/ # Environment configurations
├── libs/
│ ├── common/ # Platform-agnostic core (NEVER import Angular/Node here)
│ │ ├── src/vault/ # Cipher models and services
│ │ ├── src/auth/ # Auth types and utilities
│ │ └── src/platform/ # Platform abstractions
│ ├── angular/ # Shared Angular components/services
│ ├── auth/ # Authentication (login strategies, guards)
│ ├── key-management/ # Cryptographic operations
│ ├── state/ # State provider framework
│ ├── vault/ # Vault UI components
│ └── components/ # UI component library (Tailwind)
└── bitwarden_license/ # Commercial/Enterprise features
├── bit-browser/ # Licensed browser features
├── bit-cli/ # Licensed CLI features
├── bit-common/ # Licensed common services
└── bit-web/ # Licensed web features
```
### Key Principles
1. **Dependency Boundaries**: `libs/` cannot import from `apps/`; `libs/common` cannot import Angular or Node
2. **OSS vs Commercial Separation**: Open-source builds exclude `bitwarden_license/`; commercial builds include it
3. **Platform Abstraction**: Use service abstractions (e.g., `BrowserApi`) instead of direct platform APIs
4. **Zero-Knowledge Architecture**: All encryption/decryption happens client-side; server never sees plaintext
### Core Patterns
#### Login Strategy Pattern
**Purpose**: Handle diverse authentication methods (password, SSO, passkey, device auth) through Strategy Design Pattern
**Implementation** (`libs/auth/src/common/login-strategies/`):
```typescript
// Each auth method has its own strategy extending the base
export abstract class LoginStrategy {
abstract logIn(credentials: Credentials): Promise<AuthResult>;
protected async startLogIn(): Promise<AuthResult> {
// POST to /connect/token endpoint
// Process IdentityTokenResponse, IdentityTwoFactorResponse, or IdentityDeviceVerificationResponse
// Returns AuthResult for routing decisions
}
}
// Available strategies:
// - PasswordLoginStrategy
// - SsoLoginStrategy
// - AuthRequestLoginStrategy (Login with Device)
// - WebAuthnLoginStrategy (Passkey)
// - UserApiLoginStrategy (API Key - CLI only)
```
**Usage**:
```typescript
// LoginStrategyService orchestrates strategy selection
const credentials = new PasswordLoginCredentials(email, masterPassword);
const authResult = await loginStrategyService.logIn(credentials);
if (authResult.requiresTwoFactor) {
// Route to 2FA component
} else if (authResult.requiresDeviceVerification) {
// Route to device verification
}
```
#### State Provider Pattern
**Purpose**: Enforce consistent state management with account switching support and clear ownership
**Implementation** (`libs/state/`):
```typescript
// Define state location and namespace
export const VAULT_DISK = new StateDefinition("vault", "disk");
// Define specific state key with cleanup behavior
const CIPHERS_STATE = new UserKeyDefinition<CipherData[]>(
VAULT_DISK,
"ciphers",
{
deserializer: (data) => data?.map(c => new CipherData(c)) ?? [],
clearOn: ["logout"], // Clear on logout, not lock
}
);
// Access via StateProvider
class CipherService {
constructor(private stateProvider: StateProvider) {}
getCiphers$(userId: UserId): Observable<CipherData[]> {
return this.stateProvider.getUser(userId, CIPHERS_STATE).state$;
}
async updateCipher(userId: UserId, cipher: CipherData): Promise<void> {
await this.stateProvider.getUser(userId, CIPHERS_STATE).update(
(state) => [...state.filter(c => c.id !== cipher.id), cipher],
{ shouldUpdate: (current) => !this.isEqual(current, cipher) }
);
}
}
```
#### Angular Guard Pattern
**Purpose**: Protect routes based on authentication state, lock status, and permissions
**Implementation** (`libs/angular/src/auth/guards/`):
```typescript
// Auth guard - redirects unauthenticated users
export const authGuard = (): CanActivateFn => {
return async () => {
const authService = inject(AuthService);
const router = inject(Router);
const authStatus = await firstValueFrom(authService.authStatus$);
if (authStatus === AuthenticationStatus.Unlocked) {
return true;
}
return router.createUrlTree(["/login"]);
};
};
// Organization permission guard
export const orgPermissionsGuard = (permissions: OrganizationPermission[]): CanActivateFn => {
return async (route) => {
const organizationService = inject(OrganizationService);
const org = await organizationService.get(route.params.organizationId);
return permissions.every(p => org.hasPermission(p));
};
};
```
---
## Development Guide
### Adding a New Vault Item Type
Step-by-step checklist for adding a new cipher type (e.g., the SSH Key type added recently).
**1. Define the Enum** (`libs/common/src/vault/enums/cipher-type.ts`)
```typescript
// Use const object pattern (NOT TypeScript enum per ADR-0025)
export const CipherType = {
Login: 1,
SecureNote: 2,
Card: 3,
Identity: 4,
SshKey: 5, // New type
} as const;
export type CipherType = (typeof CipherType)[keyof typeof CipherType];
```
**2. Create Domain Model** (`libs/common/src/vault/models/domain/ssh-key.ts`)
```typescript
export class SshKey extends Domain {
privateKey: EncString;
publicKey: EncString;
fingerprint: EncString;
constructor(obj?: SshKeyData) {
super();
if (obj == null) return;
this.privateKey = new EncString(obj.privateKey);
this.publicKey = new EncString(obj.publicKey);
this.fingerprint = new EncString(obj.fingerprint);
}
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<SshKeyView> {
// Implement decryption
}
}
```
**3. Create View Model** (`libs/common/src/vault/models/view/ssh-key.view.ts`)
```typescript
export class SshKeyView implements View {
privateKey: string;
publicKey: string;
fingerprint: string;
static fromJSON(obj: Partial<Jsonify<SshKeyView>>): SshKeyView {
return Object.assign(new SshKeyView(), obj);
}
}
```
**4. Create Data Model** (`libs/common/src/vault/models/data/ssh-key.data.ts`)
```typescript
export class SshKeyData {
privateKey: string;
publicKey: string;
fingerprint: string;
constructor(response?: SshKeyApi) {
if (response == null) return;
this.privateKey = response.privateKey;
this.publicKey = response.publicKey;
this.fingerprint = response.fingerprint;
}
}
```
**5. Update Cipher Model** (`libs/common/src/vault/models/domain/cipher.ts`)
```typescript
// Add property and constructor case
export class Cipher extends Domain {
sshKey?: SshKey;
constructor(obj?: CipherData) {
// ...existing code...
switch (this.type) {
case CipherType.SshKey:
this.sshKey = new SshKey(obj.sshKey);
break;
}
}
}
```
**6. Create UI Components** (platform-specific in `apps/` or shared in `libs/vault/`)
```typescript
@Component({
selector: 'app-ssh-key-view',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush, // Required
})
export class SshKeyViewComponent {
protected cipher = input.required<CipherView>(); // Signal input
protected copyText = inject(CopyService);
}
```
**7. Write Tests** (`libs/common/src/vault/models/domain/ssh-key.spec.ts`)
```typescript
describe("SshKey", () => {
it("should decrypt ssh key fields", async () => {
const data = new SshKeyData(mockResponse);
const sshKey = new SshKey(data);
const view = await sshKey.decrypt("orgId", mockKey);
expect(view.privateKey).toBe("decrypted-private-key");
});
});
```
### Common Patterns
#### Creating an Angular Component (Modern Pattern)
```typescript
import { Component, ChangeDetectionStrategy, inject, input, output } from "@angular/core";
@Component({
selector: "app-example",
templateUrl: "./example.component.html",
// standalone: true is default, omit it
changeDetection: ChangeDetectionStrategy.OnPush, // REQUIRED
imports: [CommonModule, BitButtonModule],
})
export class ExampleComponent {
// Use inject() instead of constructor injection
protected router = inject(Router);
protected cipherService = inject(CipherService);
// Signal inputs/outputs preferred
cipherId = input.required<string>();
onSave = output<CipherView>();
// Use protected for template-accessible, private for internal
protected loading = signal(false);
protected async save(): Promise<void> {
this.loading.set(true);
try {
const cipher = await this.cipherService.save(this.cipherId());
this.onSave.emit(cipher);
} finally {
this.loading.set(false);
}
}
}
```
#### Service with State Management
```typescript
@Injectable({ providedIn: "root" })
export class FolderService {
constructor(
private stateProvider: StateProvider,
private apiService: ApiService,
) {}
// Expose as observable, take userId explicitly
folders$(userId: UserId): Observable<FolderView[]> {
return this.stateProvider.getUser(userId, FOLDERS_STATE).state$.pipe(
map(folders => folders?.map(f => new FolderView(f)) ?? [])
);
}
async createFolder(userId: UserId, name: string): Promise<void> {
// API call first
const response = await this.apiService.postFolder({ name });
// Then update local state
await this.stateProvider.getUser(userId, FOLDERS_STATE).update(
state => [...state, new FolderData(response)]
);
}
}
```
#### Error Handling
```typescript
// Use typed errors for catchable conditions
export class AuthenticationError extends Error {
constructor(
message: string,
public readonly twoFactorRequired: boolean = false,
) {
super(message);
}
}
// In services, throw specific errors
async login(credentials: PasswordLoginCredentials): Promise<AuthResult> {
try {
return await this.loginStrategyService.logIn(credentials);
} catch (e) {
if (e instanceof ErrorResponse && e.statusCode === 400) {
throw new AuthenticationError("Invalid credentials");
}
throw e;
}
}
```
---
## Data Models
### Core Types
```typescript
// Cipher - The encrypted vault item
interface Cipher {
id: string;
organizationId?: string;
folderId?: string;
type: CipherType;
name: EncString;
notes?: EncString;
login?: Login;
card?: Card;
identity?: Identity;
secureNote?: SecureNote;
sshKey?: SshKey;
fields?: Field[];
attachments?: Attachment[];
collectionIds: string[];
favorite: boolean;
reprompt: CipherRepromptType;
revisionDate: Date;
deletedDate?: Date;
}
// CipherView - Decrypted cipher for display
interface CipherView {
id: string;
name: string; // Decrypted
notes?: string; // Decrypted
login?: LoginView;
// ...other decrypted fields
}
// UserId - Branded type for type safety
type UserId = Opaque<string, "UserId">;
// EncString - Encrypted string container
class EncString {
encryptionType: EncryptionType;
data: string;
iv?: string;
mac?: string;
static async encrypt(plaintext: string, key: SymmetricCryptoKey): Promise<EncString>;
async decrypt(key: SymmetricCryptoKey): Promise<string>;
}
```
### State Definitions
```typescript
// State definitions follow pattern: DOMAIN_STORAGE_LOCATION
export const VAULT_DISK = new StateDefinition("vault", "disk");
export const AUTH_MEMORY = new StateDefinition("auth", "memory");
// Key definitions for specific data
const CIPHERS_KEY = new UserKeyDefinition<Record<string, CipherData>>(
VAULT_DISK,
"ciphers",
{
deserializer: (data) => data ?? {},
clearOn: ["logout"], // "logout", "lock", or both
}
);
// Global state (not user-scoped)
const ENVIRONMENT_KEY = new KeyDefinition<EnvironmentData>(
ENVIRONMENT_DISK,
"environment",
{ deserializer: (data) => data }
);
```
---
## Security & Configuration
### Security Rules
**MANDATORY - These rules have no exceptions:**
1. **Never log sensitive data**: No passwords, keys, tokens, or plaintext vault data in console or logs
2. **Always use EncString for sensitive storage**: Never store plaintext credentials or vault content
3. **Validate all user input**: Use TypeScript types and runtime validation at system boundaries
4. **Never bypass user verification**: Master password reprompt and biometric checks are security features
5. **Clear keys on lock/logout**: Use `clearOn` in `UserKeyDefinition` to ensure proper cleanup
6. **No direct browser APIs in extension**: Always use `BrowserApi` abstraction for cross-browser safety
7. **Respect rate limiting**: Handle 429 responses gracefully; never retry aggressively
### Security Functions
| Function | Purpose | Usage |
|----------|---------|-------|
| `encryptService.encrypt()` | Encrypt plaintext with user key | All vault data before storage |
| `cryptoService.getKey()` | Get current user's encryption key | Decrypt operations only |
| `userVerificationService.verify()` | Confirm user identity | Before sensitive operations |
| `lockService.lock()` | Clear decrypted data from memory | Timeout or manual lock |
| `logoutService.logout()` | Full account cleanup | User logout or session end |
### Environment Configuration
| Variable | Required | Description | Example |
|----------|----------|-------------|---------|
| `NODE_ENV` | No | Build environment | `development`, `production` |
| `BW_RESPONSE` | No (CLI) | Output JSON format | `true` |
| `BW_QUIET` | No (CLI) | Suppress non-essential output | `true` |
| `BW_CLEANEXIT` | No (CLI) | Exit 0 even on errors | `true` |
| `BW_SESSION` | No (CLI) | Session key for unlocked vault | `<session-token>` |
### Web Configuration Files
Located in `apps/web/config/`:
- `base.json` - Default configuration
- `development.json` - Local development (`npm run build:bit:dev:watch`)
- `cloud.json` - Production cloud deployment
- `selfhosted.json` - Self-hosted instances
```json
// Example: apps/web/config/development.json
{
"dev": true,
"urls": {
"base": "https://vault.bitwarden.com",
"api": "http://localhost:4000",
"identity": "http://localhost:33656"
}
}
```
### Authentication & Authorization
- **Master Password**: Derives master key via PBKDF2/Argon2; master key encrypts user key
- **Two-Factor Authentication**: TOTP, email, hardware keys (YubiKey), Duo
- **SSO**: SAML 2.0 and OpenID Connect with optional Key Connector
- **Trusted Device Encryption**: Device-stored keys for passwordless unlock
- **Organization Roles**: Owner, Admin, Manager, User with granular permissions
---
## Testing
### Test Structure
```
<project>/
├── src/
│ ├── feature/
│ │ ├── feature.component.ts
│ │ └── feature.component.spec.ts # Co-located unit tests
│ └── services/
│ ├── feature.service.ts
│ └── feature.service.spec.ts
└── jest.config.js
```
### Writing Tests
**Unit Test Template**:
```typescript
import { mock, MockProxy } from "jest-mock-extended";
describe("CipherService", () => {
let service: CipherService;
let stateProvider: MockProxy<StateProvider>;
let apiService: MockProxy<ApiService>;
beforeEach(() => {
stateProvider = mock<StateProvider>();
apiService = mock<ApiService>();
service = new CipherService(stateProvider, apiService);
});
describe("getCipher", () => {
it("should return decrypted cipher", async () => {
// Arrange
const mockCipherData = createMockCipherData();
stateProvider.getUser.mockReturnValue({
state$: of({ [mockCipherData.id]: mockCipherData })
});
// Act
const result = await firstValueFrom(service.getCipher$(userId, mockCipherData.id));
// Assert
expect(result.name).toBe("Test Cipher");
});
});
});
```
**State Testing with FakeStateProvider**:
```typescript
import { FakeStateProvider } from "@bitwarden/common/spec";
describe("FolderService with state", () => {
let stateProvider: FakeStateProvider;
let service: FolderService;
beforeEach(() => {
stateProvider = new FakeStateProvider();
service = new FolderService(stateProvider);
});
it("should update folder state", async () => {
// Arrange
const userId = "user-id" as UserId;
const fakeState = stateProvider.getUser(userId, FOLDERS_KEY);
fakeState.nextState([]);
// Act
await service.createFolder(userId, "New Folder");
// Assert
expect(fakeState.state).toHaveLength(1);
expect(fakeState.state[0].name).toBe("New Folder");
});
});
```
### Running Tests
```bash
# Run all tests
npm test
# Run tests for specific project
npx nx test web
npx nx test cli
npx nx test @bitwarden/common
# Run specific test file
npx nx test web -- --testPathPattern="cipher.service.spec.ts"
# Run with coverage
npm test -- --coverage
# Watch mode for development
npx nx test web -- --watch
```
### Test Utilities
- `libs/core-test-utils/` - Async test tools for state and clients
- `libs/state-test-utils/` - `FakeStateProvider`, `FakeGlobalState`, `FakeUserState`
- `libs/storage-test-utils/` - Mock storage implementations
- `jest-mock-extended` - Type-safe mocking library (preferred)
---
## Code Style & Standards
### Formatting
- **Prettier**: Auto-formatting with `npm run prettier`
- **Line width**: 100 characters
- **Quotes**: Double quotes for strings
- **Semicolons**: Required
- **Trailing commas**: ES5 style
### Naming Conventions
- `camelCase` for: variables, functions, methods, properties
- `PascalCase` for: classes, interfaces, types, enums (const objects), components
- `SCREAMING_SNAKE_CASE` for: `StateDefinition` exports (e.g., `VAULT_DISK`)
- `kebab-case` for: file names, CSS classes, Angular selectors
### Imports
```typescript
// 1. External packages (alphabetized)
import { Component, inject } from "@angular/core";
import { Observable } from "rxjs";
// 2. @bitwarden/* packages
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
import { ButtonModule } from "@bitwarden/components";
// 3. Relative imports
import { FeatureComponent } from "./feature.component";
```
### Key ESLint Rules
- `no-console: error` - Use logging services, never `console.log()`
- `@typescript-eslint/no-floating-promises: error` - Always handle promises
- `@typescript-eslint/no-explicit-any: error` - Avoid `any` types
- `curly: ["error", "all"]` - Always use braces for blocks
- `@typescript-eslint/explicit-member-accessibility` - Explicit `protected`/`private` (no `public`)
- `@angular-eslint/prefer-on-push-component-change-detection` - OnPush required
### Tailwind CSS
- Use `tw-` prefix for all Tailwind classes (e.g., `tw-flex tw-gap-2`)
- Component library in `libs/components` with shared Tailwind config
- Custom utilities defined in `tailwind.config.js`
### Pre-commit Hooks
Husky runs automatically:
- `npm run lint` - ESLint checks
- `npm run prettier` - Format verification
- Commit message format validation
---
## Anti-Patterns
### DO
- ✅ Use `inject()` function for dependency injection in components
- ✅ Use signal inputs/outputs (`input()`, `output()`) over decorators
- ✅ Use `OnPush` change detection on all components
- ✅ Use `takeUntilDestroyed()` for observable subscriptions
- ✅ Use `SingleUserState` with explicit `userId` (not `ActiveUserState`)
- ✅ Use `BrowserApi` for all browser extension API calls
- ✅ Use `CliUtils.writeLn()` for CLI output
- ✅ Use const objects with type aliases instead of TypeScript enums
- ✅ Extract business logic to services, keep components thin
- ✅ Use `shouldUpdate` option in state updates to avoid redundant writes
### DON'T
- ❌ Use TypeScript `enum` keyword (use const objects per ADR-0025)
- ❌ Use `ngClass` or `ngStyle` (use `[class.*]`/`[style.*]` bindings)
- ❌ Use code regions (`#region`/`#endregion`)
- ❌ Use `ActiveUserState.update()` (deprecated, causes race conditions)
- ❌ Use `firstValueFrom()` immediately after state update (use returned value)
- ❌ Import `apps/` from `libs/`
- ❌ Import Angular/Node modules in `libs/common`
- ❌ Use direct `chrome.*`/`browser.*` APIs in extension code
- ❌ Use `console.log()` anywhere (use logging services)
- ❌ Store plaintext sensitive data in localStorage/sessionStorage
- ❌ Skip master password reprompt or user verification checks
- ❌ Hardcode API URLs or credentials
---
## Deployment
### Building
```bash
# Development builds (with hot reload)
npm run build:bit:dev:watch --prefix apps/web
npm run build:watch:chrome --prefix apps/browser
npm run build:dev --prefix apps/desktop
# Production builds
npx nx build web --configuration=commercial
npx nx build cli --configuration=oss
npx nx build browser --configuration=commercial
# Output locations
# - dist/apps/<app>/<configuration>/
# - Example: dist/apps/cli/oss-dev/bw.js
```
### Versioning
Follow semantic versioning: `MAJOR.MINOR.PATCH`
- **MAJOR**: Breaking API changes, major feature overhauls
- **MINOR**: New features, backwards-compatible enhancements
- **PATCH**: Bug fixes, security patches
### CI/CD
- GitHub Actions workflows in `.github/workflows/`
- `build-*.yml` - Build and test each application
- Artifacts published to respective stores (Chrome Web Store, npm, etc.)
---
## Troubleshooting
### Common Issues
#### Browser Extension Not Loading
**Problem**: Extension fails to load in Chrome/Firefox
**Solution**:
1. Check `npm ci` completed successfully
2. Run `npm run build:watch:chrome --prefix apps/browser`
3. Load unpacked from `apps/browser/dist/`
4. Check browser console for errors
5. Verify manifest.json is valid for target browser
#### State Not Persisting
**Problem**: User data disappears after refresh
**Solution**:
1. Verify `StateDefinition` uses `"disk"` not `"memory"`
2. Check `clearOn` setting in `UserKeyDefinition`
3. Ensure proper `deserializer` implementation
4. Check browser storage limits (extension context)
#### Desktop IPC Errors
**Problem**: Communication failure between main and renderer
**Solution**:
1. Check preload script exports the IPC method
2. Verify contextIsolation settings in webPreferences
3. Ensure renderer isn't importing Node modules directly
4. Check main process logs for errors
#### CLI Session Issues
**Problem**: CLI reports "vault is locked" after unlock
**Solution**:
1. Export session: `export BW_SESSION=$(bw unlock --raw)`
2. Use `--session` flag: `bw list items --session <token>`
3. Check `BW_SESSION` environment variable is set
### Debug Tips
- **Enable verbose logging**: Set `logging.level` in configuration
- **Browser extension**: Use `chrome://extensions` -> Inspect service worker
- **Desktop**: Use `--inspect` flag for Node debugging
- **Web**: Browser DevTools with Angular DevTools extension
- **CLI**: Use `BW_DEBUG=true` environment variable
---
## References
### Official Documentation
- [Contributing Documentation](https://contributing.bitwarden.com/)
- [Clients Getting Started](https://contributing.bitwarden.com/getting-started/clients/)
- [Bitwarden Help Center](https://bitwarden.com/help/)
- [Security Whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
### Internal Documentation
- [Login Strategies README](libs/auth/src/common/login-strategies/README.md)
- [State Provider README](libs/state/README.md)
- [Using Nx Guide](docs/using-nx-to-build-projects.md)
- [Angular Modernization Skill](.claude/skills/angular-modernization/)
### Security & Reporting
- [Security Policy](SECURITY.md) - Responsible disclosure via HackerOne
- [HackerOne Program](https://hackerone.com/bitwarden/)
### Related Repositories
- [bitwarden/server](https://github.com/bitwarden/server) - Backend API
- [bitwarden/ios](https://github.com/bitwarden/ios) - iOS app
- [bitwarden/android](https://github.com/bitwarden/android) - Android app