1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-02-26 01:13:14 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Thomas Rittson
b1d7920d73 Enable experimental vm modules to see if it fixes google sync error 2025-10-07 20:26:32 +10:00
230 changed files with 29228 additions and 12212 deletions

View File

@@ -1,619 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Bitwarden Directory Connector
## Project Overview
Directory Connector is a TypeScript application that synchronizes users and groups from directory services to Bitwarden organizations. It provides both a desktop GUI (built with Angular and Electron) and a CLI tool (bwdc).
**Supported Directory Services:**
- LDAP (Lightweight Directory Access Protocol) - includes Active Directory and general LDAP servers
- Microsoft Entra ID (formerly Azure Active Directory)
- Google Workspace
- Okta
- OneLogin
**Technologies:**
- TypeScript
- Angular (GUI)
- Electron (Desktop wrapper)
- Node
- Jest for testing
### Current Project Status
**Mission Critical but Deprioritized:** Directory Connector is used to sync customer directory services with their Bitwarden organization. While SCIM is the more modern cloud-hosted solution, not all directory services support SCIM, and SCIM is only available on Enterprise plans. Therefore, DC remains mission-critical infrastructure for many paying customers, but it's deprioritized in the codebase due to infrequent changes.
**Isolated Repository:** Unlike other Bitwarden client applications that live in a monorepo with shared core libraries, Directory Connector was kept separate when other TypeScript clients moved to the monorepo. It got its own copy of the jslib repo to avoid unnecessary regressions from apparently unrelated code changes in other clients. This severed it from the rest of the codebase, causing:
- Outdated dependencies that can't be updated (ES modules vs CommonJS conflicts)
- File/folder structure that doesn't match modern Bitwarden client patterns
- Accumulated technical debt requiring significant investment to pay down
- jslib contains unused code from all clients, but cannot be deleted due to monolithic/tightly coupled architecture
**Critical Issues (Current Status):**
-~~Electron, Node, and Angular are on unmaintained versions~~ **RESOLVED** - All updated (Electron 39, Node 20, Angular 21, TypeScript 5.9)
-`keytar` is archived (Dec 2022) and incompatible with Node v22, **blocking Node upgrades beyond v20** - **PRIMARY BLOCKER**
- ❌ No ESM support blocks dependency upgrades: googleapis, lowdb, chalk, inquirer, node-fetch, electron-store
- ⚠️ 70 dev dependencies + 31 runtime dependencies = excessive maintenance burden (count increased with Angular 21 tooling)
- ❌ StateService is a large pre-StateProvider monolith containing every getter/setter for all clients (PM-31159 In Progress)
-~~Angular CLI not used~~ **RESOLVED** - Angular CLI 21.1.2 now integrated with angular.json configuration
**Development Approach:** When working on this codebase, prioritize sustainability and maintainability over adding new features. Consider how changes will affect long-term maintenance burden.
## Tech Debt Roadmap
### Progress Summary
**Completed:**
- ✅ Phase 0 (Immediate Priority): All major dependencies upgraded (Node 20, Angular 21, TypeScript 5.9, Electron 39)
- ✅ Phase 6: Angular CLI integration complete
**In Progress:**
- 🔄 Phase 1: StateService rewrite (PM-31159)
**Blocked/Todo:**
- ❌ Phase 2: Remove remaining jslib code (blocked by Phase 1)
- ❌ Phase 3: Repository restructure (should be done before Phase 5)
- ⚠️ Phase 4: Replace Keytar **[CRITICAL BLOCKER]** - blocking Node v22+ upgrades
- ❌ Phase 5: ESM Support (blocked by Phase 3, needed for googleapis, lowdb, chalk, inquirer, etc.)
**Primary Blocker:** Keytar removal (Phase 4) is the most critical task as it blocks Node upgrades beyond v20.
---
### ✅ Immediate Priority: Unsupported Dependencies (COMPLETED)
**Upgrade Path (July 2025 release) - STATUS: COMPLETE**
All major version upgrades have been completed and exceeded targets:
1. ✅ Node 18.20.8 → 20.18 → **COMPLETE** (engines: `~20`, .nvmrc: `v20`)
2. ✅ Angular 17 → 18.2.x → **EXCEEDED** (now at **21.1.1**)
3. ✅ TypeScript 5.4.5 → 5.6.0 → **EXCEEDED** (now at **5.9.3**)
4. ✅ Electron 34 → 36 → **EXCEEDED** (now at **39.2.1**)
5. ✅ Angular matches clients monorepo version (21.x)
**Current Versions:**
- Node: v20 (project target), blocked from v22+ by keytar
- TypeScript: 5.9.3
- Angular: 21.1.1 (all packages)
- Electron: 39.2.1 (well beyond EOL target of 36)
- @yao-pkg/pkg: 5.16.1 (community fork replacing archived pkg)
**Note:** Further Node upgrades to v22+ are **blocked by keytar** (see Phase 4). Electron 36 was EOL October 2028, but we're already on 39.2.1.
### Phase 1: StateService Rewrite (PM-31159, In Progress)
**Problem:** StateService is a post-account-switching, pre-StateProvider monolith containing every getter/setter for all clients. This prevents deletion of unused data models and code. Never very stable, and more complex than DC needs (DC doesn't need account switching).
**Current Status:** 🔄 **Active PR** - [#990](https://github.com/bitwarden/directory-connector/pull/990) (Open, Author: @BTreston)
- PR created: Feb 2, 2026
- Last updated: Feb 5, 2026
- Files changed: 17 files (+1,512, -41 lines)
- Commits: 4 (scaffold, add tests, fix type issues, fix integration test)
**Implementation Details:**
**New Architecture:**
- Created `StateServiceVNext` interface (`src/abstractions/state-vNext.service.ts`)
- New implementation: `StateServiceVNextImplementation` (`src/services/state-service/state-vNext.service.ts`)
- New state model with flat key-value structure (`src/models/state.model.ts`)
- Comprehensive test suite: `state-vNext.service.spec.ts` (488 lines of tests)
**Storage Key Structure:**
```typescript
// vNext Storage Keys (Flat key-value structure)
StorageKeysVNext = {
stateVersion: "stateVersion",
directoryType: "directoryType",
organizationId: "organizationId",
directory_ldap: "directory_ldap",
directory_gsuite: "directory_gsuite",
directory_entra: "directory_entra",
directory_okta: "directory_okta",
directory_onelogin: "directory_onelogin",
sync: "sync",
syncingDir: "syncingDir",
};
// Secure storage keys for sensitive data
SecureStorageKeysVNext = {
ldap: "secret_ldap",
gsuite: "secret_gsuite",
azure: "secret_azure", // Backwards compatible with old name
entra: "secret_entra",
okta: "secret_okta",
oneLogin: "secret_oneLogin",
userDelta: "userDeltaToken",
groupDelta: "groupDeltaToken",
lastUserSync: "lastUserSync",
lastGroupSync: "lastGroupSync",
lastSyncHash: "lastSyncHash",
};
```
**Migration Strategy:**
- State version bumped to `StateVersion.Five` (`jslib/common/src/enums/stateVersion.ts`)
- Enhanced `StateMigrationService` to handle migration from old account-based structure to new flat structure
- Migration keys defined for backwards compatibility (`MigrationKeys`, `SecureStorageKeysMigration`)
- Temporary keys used during migration (`TempKeys`) to preserve data during transition
**File Organization:**
- State-related files moved to `src/services/state-service/` subdirectory:
- `state-vNext.service.ts` (new implementation)
- `state-vNext.service.spec.ts` (488 lines of tests)
- `state.service.ts` (legacy, moved from `src/services/`)
- `stateMigration.service.ts` (enhanced for v5 migration)
- New abstraction: `src/abstractions/state-vNext.service.ts`
- New model: `src/models/state.model.ts` (defines all storage keys)
**Integration:**
- Both old `StateService` and new `StateServiceVNext` injected in parallel during migration phase
- `DirectoryFactoryService` updated to accept both services
- Services module provides both implementations
- CLI (`bwdc.ts`) and GUI (`main.ts`) both instantiate new service alongside old one
**Chosen Approach Benefits:**
- Clean break with old StateService - high degree of certainty
- Simple and focused on DC's needs (no account switching, no rxjs)
- Flat key-value structure easier to maintain
- Versioning and migration capabilities included
- Keeps existing data.json around during transition
- All getters/setters in one place (acceptable for small application)
**Rejected Approaches:**
- Copy StateProvider from clients: Too complex (supports account switching, rxjs, syncing background/foreground contexts)
- Rewrite simplified StateService keeping current data structure: Commits us to previous decisions, keeps monolithic account objects
**Next Steps:**
- Complete PR review and merge
- Monitor for regressions during initial rollout
- After several releases, can remove old StateService and migration code
- Begin Phase 2: Remove remaining jslib code that was only needed by old StateService
### Phase 2: Remove Remaining jslib Code
After StateService is removed, review and delete old models and remaining services that referenced each other. jslib contains unused code from all clients that DC doesn't need.
### Phase 3: Restructure Repository (PM-31852, To Do)
**Current Structure:**
```
src/ # Both Electron and CLI app code
src-cli/ # package.json entry point for CLI only, no code
jslib/
├── common/ # Shared common code
├── node/ # Node specific code used in CLI
└── electron/ # Electron specific code used in GUI
```
**Target Structure:**
```
src-gui/ # Electron specific code only (combining src (partial) + jslib/electron)
src-cli/ # Node and CLI specific code only (combining src (partial) + jslib/node)
libs/ # Shared app-independent DC code, e.g. sync services (combining src (partial) + jslib/common)
```
**Why:** Makes subsequent changes (code reorganizing, ESM support) much easier. This should be done early in the modernization process.
### Phase 4: Replace Keytar (PM-12436, To Do) ⚠️ **CRITICAL BLOCKER**
**Problem:** `keytar` (OS secure storage for secrets) was archived December 2022 and is incompatible with Node v22, **actively blocking Node upgrades beyond v20**.
**Current Status:**
- `keytar`: **7.9.0** (still present in dependencies)
- **This is the #1 blocker preventing Node v22+ upgrades**
- All "Immediate Priority" dependencies have been upgraded, but further progress requires removing keytar
**Solution:** Migrate to Bitwarden's Rust implementation in `desktop_native` (same as clients monorepo did)
1. Implement Rust <-> NAPI integration (like `desktop_native/napi`) from Electron app to Rust code
2. Copy, rename, and expose necessary functions
3. Point to `desktop_native` crate using git link from DC repo (no need for SDK yet):
```rust
desktop_core = { git = "https://github.com/bitwarden/clients", rev = "00cf24972d944638bbd1adc00a0ae3eeabb6eb9a" }
```
**Important:** `keytar` uses wrong encoding on Windows (UTF-8 instead of UTF-16). Bitwarden uses UTF-16. Code should contain a migration - ensure old values are migrated correctly during testing.
**Priority:** This should be prioritized as it's blocking the Node upgrade path and has been archived for over 2 years.
### Phase 5: Add ESM Support (PM-31850, To Do)
**Problem:** No ESM module support prevents upgrading key dependencies.
**Blocked Dependencies (Current Status):**
- ❌ `googleapis`: **149.0.0** → current (major dependency, disabled in renovate.json5)
- ❌ `lowdb`: **1.0.0** → v7
- ❌ `@types/lowdb`: **1.0.15** (can be deleted once inquirer is upgraded)
- ❌ `@electron/notarize`: **2.5.0** → v3.0.1
- ❌ `chalk`: **4.1.2** → v5.3.0
- ❌ `inquirer`: **8.2.6** → v12.1.0
- ❌ `@types/inquirer`: **8.2.10** (should be deleted when inquirer upgraded)
- ❌ `node-fetch`: **2.7.0** → v3.3.2 (should use native Node fetch API when on Node >=21)
- ❌ `electron-store`: **8.2.0** → v10.1.0
**Status:** These dependencies remain blocked as expected. They will stay on old versions until:
1. Phase 3 (Repository Restructure) is complete
2. ESM support is implemented
3. Note: These ESM dependencies are primarily used in CLI build, so restructuring first (Phase 3) will limit the impact of ESM migration.
**Implementation:**
1. Update tsconfig.json and package.json configurations
2. Update import/export syntax to no longer use `require` statements
3. Upgrade dependencies to move away from CommonJS (ESM can import CommonJS, but not vice versa)
4. Trial and error
**Reference:** [Pure ESM package guide](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
### Phase 6: Add Angular CLI (PM-31849, In Progress / Possibly Complete?)
**Problem:** Angular CLI provides great DX and makes it easier to manage Angular changes (e.g. auto-migrations). DC didn't use it.
**Current Status:**
- ✅ `@angular/cli`: **21.1.2** is now present in **runtime dependencies**
- ✅ `@angular/build`: **21.1.2** is present in dev dependencies
- ✅ All Angular tooling has been updated to v21.x
**Status:** ✅ **COMPLETE** - Angular CLI has been successfully integrated:
- `angular.json` configuration file exists
- `.angular/` cache directory present
- `@angular/cli` 21.1.2 in runtime dependencies
- `@angular/build` 21.1.2 in dev dependencies
- All Angular packages updated to v21.x
This migration provides improved DX and access to Angular's auto-migration tools for future updates.
### Additional Considerations
**Reduce Dependency Count:** Current state is 70 dev dependencies + 31 runtime dependencies (101 total). The dev dependency count increased from the original 66 due to Angular 21 upgrade adding additional tooling. After removing old code, review dependency list:
- Can we remove some after code cleanup?
- Could we reintegrate with monorepo to leverage Component Library and shared platform dependencies?
- **Risk:** Becomes tightly coupled with monorepo code → regression risk, move slower due to coupling
**GitHub Workflows:** Need review and modernization:
- PM-20478: Add check-run workflow for CI on community PRs
- PM-18290: Add linting workflow
- PM-18289: Update build workflow
- `pkg` and `pkg-fetch` for packaging Node runtime in CLI release are archived (fork exists but untrusted; clients vets all changes manually)
- Options: Make our own fork, or use Node's single executable binary support (investigate)
## Common Development Commands
### Desktop App (Electron + Angular)
**Initial Setup:**
```bash
npm install # Install dependencies (runs git submodule init automatically)
npm run rebuild # Rebuild native modules for Electron
```
**Development:**
```bash
npm run electron # Build and run desktop app with hot reload and debugging
npm run electron:ignore # Same as above but ignores certificate errors
```
**Building:**
```bash
npm run build # Build both main and renderer processes
npm run build:main # Build Electron main process only
npm run build:renderer # Build Angular renderer process only
npm run build:renderer:watch # Build renderer with file watching
```
**Distribution:**
```bash
npm run dist:mac # Create macOS distributable
npm run dist:win # Create Windows distributable
npm run dist:lin # Create Linux distributable
```
### CLI (bwdc)
**Development:**
```bash
npm run build:cli:watch # Build CLI with file watching
node ./build-cli/bwdc.js --help # Run the CLI from build output
```
**Production Build:**
```bash
npm run build:cli:prod # Build CLI for production
npm run dist:cli # Create platform-specific CLI executables (all platforms)
npm run dist:cli:mac # Create macOS CLI executable only
npm run dist:cli:win # Create Windows CLI executable only
npm run dist:cli:lin # Create Linux CLI executable only
```
### Testing
**Unit Tests:**
```bash
npm test # Run unit tests (excludes integration tests)
npm run test:watch # Run unit tests in watch mode
npm run test:watch:all # Run unit tests in watch mode (all files)
npm run test:types # Run TypeScript type checking without emitting files
```
**Integration Tests:**
```bash
npm run test:integration:setup # Set up Docker containers for LDAP testing
npm run test:integration # Run integration tests
npm run test:integration:watch # Run integration tests in watch mode
```
Integration tests require Docker and test against live directory services. The setup command creates OpenLDAP containers using docker-compose.yml.
### Linting & Formatting
```bash
npm run lint # Run ESLint and Prettier checks
npm run lint:fix # Auto-fix ESLint issues
npm run prettier # Format all files with Prettier
```
### Submodule Management
The `jslib` folder is a git submodule containing shared Bitwarden libraries:
```bash
npm run sub:update # Update submodule to latest remote version
npm run sub:pull # Pull latest changes in submodule
npm run sub:commit # Pull and commit submodule update
```
### Utility Commands
```bash
npm run reset # Remove keytar modules and reinstall (use when switching between CLI/desktop)
npm run clean:dist # Clean desktop distribution files
npm run clean:dist:cli # Clean CLI distribution files
```
**Important:** When switching between developing the desktop app and CLI, run `npm run reset` to avoid native module conflicts.
## Code Architecture & Structure
### Directory Organization
```
src/
├── abstractions/ # Interface definitions (e.g., IDirectoryService)
├── services/ # Business logic implementations for directory services, sync, auth
├── models/ # Data models (UserEntry, GroupEntry, etc.)
├── commands/ # CLI command implementations
├── app/ # Angular GUI components
└── utils/ # Test utilities and fixtures
src-cli/ # CLI-specific code (imports common code from src/)
jslib/ # Legacy folder structure (mix of deprecated/unused and current code - new code should not be added here)
```
### Key Architectural Patterns
1. **Abstractions = Interfaces**: All interfaces are defined in `/abstractions`
2. **Services = Business Logic**: Implementations live in `/services`
3. **Directory Service Pattern**: Each directory provider implements `IDirectoryService` interface
4. **Separation of Concerns**: GUI (Angular app) and CLI (commands) share the same service layer
### Core Synchronization Flow
The sync process follows this pattern:
1. **DirectoryFactoryService** (`src/services/directory-factory.service.ts`) - Creates the appropriate directory service based on DirectoryType configuration
2. **IDirectoryService** implementation (`src/services/directory-services/*.service.ts`) - Each provider (LDAP, Entra ID, Google, Okta, OneLogin) implements:
- `getEntries(force, test)` - Returns `[GroupEntry[], UserEntry[]]`
- Provider-specific authentication and API calls
3. **SyncService** (`src/services/sync.service.ts`) - Orchestrates the sync:
- Calls directory service to get entries
- Filters and deduplicates users/groups
- Uses BatchRequestBuilder or SingleRequestBuilder to format API requests
- Generates hash to detect changes and avoid redundant syncs
- Sends data to Bitwarden API via ApiService
4. **Request Builders** (`src/services/*-request-builder.ts`) - Transform directory entries into Bitwarden API format
### Shared Library (jslib)
The `jslib` folder is a git submodule containing shared Bitwarden code:
- Common services (API, Crypto, Storage, Auth)
- Platform utilities
- Shared models and abstractions
**Important:** This is legacy structure - do not add new code to jslib. New code should go in `src/`.
## Development Conventions
### Code Organization
**File Naming:**
- kebab-case for files: `ldap-directory.service.ts`
- Descriptive names that reflect purpose
**Class/Function Naming:**
- PascalCase for classes and interfaces
- camelCase for functions and variables
- Descriptive names that indicate purpose
**File Structure:**
- Keep files focused on single responsibility
- Create new service files for distinct directory integrations
- Separate models into individual files when complex
### TypeScript Conventions
**Import Patterns:**
- Use path aliases (`@/`) for project imports
- `@/` - project root
- `@/jslib/` - jslib folder
- ESLint enforces alphabetized import ordering with newlines between groups
**Type Safety:**
- Avoid `any` types - use proper typing or `unknown` with type guards
- Prefer interfaces for contracts, types for unions/intersections
- Use strict null checks - handle `null` and `undefined` explicitly
- Leverage TypeScript's type inference where appropriate
**Configuration:**
- Use configuration files or environment variables
- Never hardcode URLs or configuration values
## Security Best Practices
**Credential Handling:**
- Never log directory service credentials, API keys, or tokens
- Use secure storage mechanisms for sensitive data
- Credentials should never be hardcoded
- Store credentials encrypted, never in plain text
**Sensitive Data:**
- User and group data from directories should be handled securely
- Avoid exposing sensitive information in error messages
- Sanitize data before logging
- Be cautious with data persistence
**Input Validation:**
- Validate and sanitize data from external directory services
- Check for injection vulnerabilities (LDAP injection, etc.)
- Validate configuration inputs from users
**API Security:**
- Ensure authentication flows are implemented correctly
- Verify SSL/TLS is used for all external connections
- Check for secure token storage and refresh mechanisms
## Error Handling
**Best Practices:**
1. **Try-catch for async operations** - Always wrap external API calls
2. **Meaningful error messages** - Provide context for debugging
3. **Error propagation** - Don't swallow errors silently
4. **User-facing errors** - Separate user messages from developer logs
## Performance Best Practices
**Large Dataset Handling:**
- Use pagination for large user/group lists
- Avoid loading entire datasets into memory at once
- Consider streaming or batch processing for large operations
**API Rate Limiting:**
- Respect rate limits for Microsoft Graph API, Google Admin SDK, etc.
- Consider batching large API calls where necessary
**Memory Management:**
- Close connections and clean up resources
- Remove event listeners when components are destroyed
- Be cautious with caching large datasets
## Testing
**Framework:**
- Jest with jest-preset-angular
- jest-mock-extended for type-safe mocks with `mock<Type>()`
**Test Organization:**
- Tests colocated with source files
- `*.spec.ts` - Unit tests for individual components/services
- `*.integration.spec.ts` - Integration tests against live directory services
- Test helpers located in `utils/` directory
**Test Naming:**
- Descriptive, human-readable test names
- Example: `'should return empty array when no users exist in directory'`
**Test Coverage:**
- New features must include tests
- Bug fixes should include regression tests
- Changes to core sync logic or directory specific logic require integration tests
**Testing Approach:**
- **Unit tests**: Mock external API calls using jest-mock-extended
- **Integration tests**: Use live directory services (Docker containers or configured cloud services)
- Focus on critical paths (authentication, sync, data transformation)
- Test error scenarios and edge cases (empty results, malformed data, connection failures), not just happy paths
## Directory Service Patterns
### IDirectoryService Interface
All directory services implement this core interface with methods:
- `getUsers()` - Retrieve users from directory and transform them into standard objects
- `getGroups()` - Retrieve groups from directory and transform them into standard objects
- Connection and authentication handling
### Service-Specific Implementations
Each directory service has unique authentication and query patterns:
- **LDAP**: Direct LDAP queries, bind authentication
- **Microsoft Entra ID**: Microsoft Graph API, OAuth tokens
- **Google Workspace**: Google Admin SDK, service account credentials
- **Okta/OneLogin**: REST APIs with API tokens
## References
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
- [Contributing Guidelines](https://contributing.bitwarden.com/contributing/)
- [Code Style](https://contributing.bitwarden.com/contributing/code-style/)
- [Security Whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
- [Security Definitions](https://contributing.bitwarden.com/architecture/security/definitions)

View File

@@ -1,239 +0,0 @@
# Phase 2 PR #1: Flatten Account Model - IMPLEMENTATION COMPLETE
## Status: ✅ COMPLETED
**Implementation Date:** February 13, 2026
**All tests passing:** 120/120 ✅
**TypeScript compilation:** Success ✅
---
## Summary
Successfully implemented Phase 2 PR #1: Flatten Account Model. The Account model has been simplified from 177 lines (51 + 126 inherited) to 51 lines, removing the BaseAccount inheritance and flattening nested structures into direct properties.
## Changes Implemented
### Files Modified (7 files)
1. **`jslib/common/src/enums/stateVersion.ts`**
- Added `StateVersion.Five` for the flattened Account structure
- Updated `StateVersion.Latest = Five`
2. **`src/models/account.ts`**
- Removed `extends BaseAccount` inheritance
- Removed `ClientKeys` class (redundant)
- Flattened 6 authentication fields to top level:
- `userId`, `entityId`, `apiKeyClientId`
- `accessToken`, `refreshToken`, `apiKeyClientSecret`
- Kept `DirectoryConfigurations` and `DirectorySettings` unchanged
- Added compatibility fields with FIXME comment for jslib infrastructure:
- `data?`, `keys?`, `profile?`, `settings?`, `tokens?` (optional, unused)
- Simplified constructor without Object.assign
3. **`src/services/stateMigration.service.ts`**
- Added `migrateStateFrom3To4()` placeholder migration
- Added `migrateStateFrom4To5()` to flatten nested → flat Account structure
- Updated `migrate()` method with new case statements for v3→v4 and v4→v5
- Updated `migrateStateFrom1To2()` to use flattened structure (removed `account.profile`, `account.clientKeys`)
4. **`src/services/auth.service.ts`**
- Removed imports: `AccountKeys`, `AccountProfile`, `AccountTokens`
- Simplified account creation from 26 lines to 10 lines (62% reduction)
- Direct property assignment instead of nested objects with spread operators
5. **`src/services/state.service.ts`**
- Changed `account.profile.userId``account.userId`
- Removed `account.settings` from `scaffoldNewAccountDiskStorage`
- Added `settings` back to `resetAccount` for base class compatibility (unused but required)
6. **`src/services/authService.spec.ts`**
- Removed imports: `AccountKeys`, `AccountProfile`, `AccountTokens`
- Updated test expectations to match new flat Account structure
### Files Created (1 file)
7. **`src/services/stateMigration.service.spec.ts`**
- Comprehensive migration test suite (5 tests, 210 lines)
- Tests flattening nested account structure
- Tests handling missing nested objects gracefully
- Tests empty account list
- Tests preservation of directory configurations and settings
- Tests state version update
## Code Reduction Achieved
- **Account model:** 177 lines (51 + 126 inherited) → 51 lines (71% reduction)
- **AuthService account creation:** 26 lines → 10 lines (62% reduction)
- **Import statements removed:** 5 jslib imports across multiple files
## Migration Logic
### State Version v4 → v5 Migration
The `migrateStateFrom4To5()` method handles conversion from nested to flat structure:
```typescript
// OLD (nested structure):
{
profile: {
userId: "CLIENT_ID",
entityId: "CLIENT_ID",
apiKeyClientId: "organization.CLIENT_ID"
},
tokens: {
accessToken: "token",
refreshToken: "refresh"
},
keys: {
apiKeyClientSecret: "secret"
}
}
// NEW (flat structure):
{
userId: "CLIENT_ID",
entityId: "CLIENT_ID",
apiKeyClientId: "organization.CLIENT_ID",
accessToken: "token",
refreshToken: "refresh",
apiKeyClientSecret: "secret"
}
```
**Migration Safety:**
- Null-safe property access with `??` operator
- Preserves all directory configurations and settings
- Falls back to userId if profile.userId doesn't exist
- Handles empty account lists gracefully
## Test Results
### Unit Tests: ✅ PASS
```
Test Suites: 14 passed, 14 total
Tests: 120 passed, 120 total
```
New tests added:
- `should flatten nested account structure`
- `should handle missing nested objects gracefully`
- `should handle empty account list`
- `should preserve directory configurations and settings`
- `should update state version after successful migration`
### TypeScript Compilation: ✅ PASS
```
npm run test:types
```
All type checks pass with zero errors.
## Technical Notes
### Compatibility Fields
Added optional compatibility fields to Account model to satisfy jslib infrastructure type constraints:
```typescript
// FIXME: Remove these compatibility fields after StateServiceVNext migration (PR #990) is merged
// These fields are unused but required for type compatibility with jslib's StateService infrastructure
data?: any;
keys?: any;
profile?: any;
settings?: any;
tokens?: any;
```
These will be removed after PR #990 (StateServiceVNext) merges and old StateService is deleted.
### Key Architectural Decision
Chose to add compatibility fields rather than refactor entire jslib infrastructure because:
1. PR #990 (StateServiceVNext) will eventually replace this infrastructure
2. Minimizes changes needed in this PR
3. Avoids conflicts with PR #990
4. Can be cleaned up later
## What This Enables
### Immediate Benefits
- ✅ Simplified Account model (71% code reduction)
- ✅ Clearer authentication field structure
- ✅ Easier debugging (no nested property access)
- ✅ Self-documenting code (obvious what DC needs)
### Enables Future Work
- **Phase 2 PR #2:** Remove StateFactory infrastructure
- **Phase 2 PR #3:** Delete ~90 unused jslib files including:
- EncString (only used by old nested Account)
- SymmetricCryptoKey (only used by old nested Account)
- OrganizationData (completely unused)
- ProviderData (completely unused)
- AccountKeys, AccountProfile, AccountTokens, AccountData, AccountSettings
## Merge Strategy
**Conflict Management:**
- This PR targets current codebase (with old StateService)
- Will conflict with PR #990 (StateServiceVNext) when it merges
- Plan: Rebase this PR after #990 merges
- Expected conflicts: StateService files, Account model structure
- Resolution: Keep StateServiceVNext changes, apply Account flattening to new structure
## Next Steps
1. **Review & Test:** Thorough code review and manual testing
2. **Create PR:** Open PR with comprehensive description and test results
3. **Manual Testing Scenarios:**
- Fresh installation → authentication flow
- Existing installation → migration runs successfully
- All directory types → configuration persists correctly
- CLI authentication → flat structure works
4. **After Merge:**
- Begin Phase 2 PR #2: Remove StateFactory Infrastructure
- Monitor for any migration issues in production
## Related Work
- **Depends On:** None (can merge independently)
- **Blocks:** Phase 2 PR #2 (Remove StateFactory), Phase 2 PR #3 (Delete Unused jslib Files)
- **Conflicts With:** PR #990 (StateServiceVNext) - plan to rebase after #990 merges
- **Part Of:** Phase 2 tech debt cleanup (see CLAUDE.md)
---
## Original Implementation Plan
[The original detailed step-by-step plan from the conversation has been preserved below for reference]
### Context
Directory Connector's Account model currently extends jslib's BaseAccount, inheriting 126 lines of complex nested structures designed for multi-account password manager features that DC doesn't use. This inheritance creates unnecessary coupling and blocks cleanup of unused jslib dependencies.
**Current State:**
- Account extends BaseAccount with nested objects: `profile.userId`, `tokens.accessToken`, `keys.apiKeyClientSecret`
- Only 6 fields from BaseAccount are actually used by DC
- 120+ lines of inherited code (AccountData, AccountKeys, AccountProfile, AccountSettings, AccountTokens) are unused
- Creates dependencies on EncString, SymmetricCryptoKey, OrganizationData, ProviderData that DC never uses
**Problem:**
- Unnecessary complexity for a single-account application
- Blocks deletion of unused jslib models (Phase 2 goal)
- Verbose account creation code (26 lines to set 6 fields)
- Difficult to understand what DC actually needs
**Goal:**
Flatten Account model to contain only the 8 fields DC uses, removing BaseAccount inheritance. This enables Phase 2 PR #2 and PR #3 to delete ~90 unused jslib files.
[Rest of original plan preserved in conversation transcript]

View File

@@ -1,130 +0,0 @@
---
userInvocable: true
---
# CommonJS to ESM Conversion
Convert a file (or files) from CommonJS module syntax to ECMAScript Modules (ESM).
## Usage
```
/commonjs-to-esm <file-path> [additional-file-paths...]
```
## Parameters
- `file-path` - Path to the file(s) to convert from CommonJS to ESM
## Examples
```
/commonjs-to-esm src/services/auth.service.ts
/commonjs-to-esm src/utils/helper.ts src/utils/parser.ts
```
## Process
This skill performs a comprehensive analysis and planning process:
### 1. Analyze Target File(s)
For each file to convert:
- Read the file contents
- Identify its purpose and functionality
- Catalog all CommonJS patterns used:
- `require()` statements
- `module.exports` assignments
- `exports.x = ...` assignments
- Dynamic requires
- `__dirname` and `__filename` usage
### 2. Find Dependents
- Search for all files that import/require the target file(s)
- Identify the import patterns used by dependents
- Map the dependency tree to understand impact scope
### 3. Analyze Dependencies
- List all modules the target file(s) depend on
- Determine if dependencies support ESM
- Identify potential blocking dependencies (CommonJS-only packages)
- Check for dynamic imports that may need special handling
### 4. Identify Conversion Challenges
Common issues to flag:
- `__dirname` and `__filename` (need `import.meta.url` conversion)
- Dynamic `require()` calls (need `import()` conversion)
- Conditional requires (need refactoring)
- JSON imports (need `assert { type: 'json' }`)
- CommonJS-only dependencies (may block conversion)
- Circular dependencies (may need restructuring)
### 5. Generate Conversion Plan
Create a step-by-step plan that includes:
**Target File Changes:**
- Convert `require()` to `import` statements
- Convert `module.exports` to `export` statements
- Update `__dirname`/`__filename` to use `import.meta.url`
- Handle dynamic imports appropriately
- Update file extensions if needed (e.g., `.js` to `.mjs`)
**Dependent File Changes:**
- Update all import statements in dependent files
- Ensure consistent naming (default vs named exports)
- Update path references if extensions change
**Configuration Changes:**
- `package.json`: Add `"type": "module"` or use `.mjs` extension
- `tsconfig.json`: Update `module` and `moduleResolution` settings
- Build tools: Update bundler/compiler configurations
**Testing Strategy:**
- Run unit tests after conversion
- Verify no runtime errors from import changes
- Check that all exports are accessible
- Test dynamic import scenarios
### 6. Risk Assessment
Evaluate:
- Number of files affected
- Complexity of CommonJS patterns used
- Presence of blocking dependencies
- Potential for breaking changes
### 7. Present Plan
Output a structured plan with:
- Summary of changes needed
- Ordered steps for execution
- List of files to modify
- Configuration changes required
- Testing checkpoints
- Risk factors and mitigation strategies
- Estimated scope (small/medium/large change)
## Notes
- ESM is **not** compatible with CommonJS in all cases - ESM can import CommonJS, but CommonJS **cannot** require ESM
- This means conversions should generally proceed from leaf dependencies upward
- Some packages remain CommonJS-only and may block full conversion
- The skill generates a plan but does NOT automatically execute the conversion - review and approve first
## References
- [Pure ESM package guide](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
- [Node.js ESM documentation](https://nodejs.org/api/esm.html)
- [TypeScript ESM support](https://www.typescriptlang.org/docs/handbook/esm-node.html)

10
.eslintignore Normal file
View File

@@ -0,0 +1,10 @@
dist
build
build-cli
webpack.cli.js
webpack.main.js
webpack.renderer.js
**/node_modules
**/jest.config.js

95
.eslintrc.json Normal file
View File

@@ -0,0 +1,95 @@
{
"root": true,
"env": {
"browser": true,
"node": true
},
"overrides": [
{
"files": ["*.ts", "*.js"],
"plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.eslint.json"],
"sourceType": "module",
"ecmaVersion": 2020
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
"plugin:rxjs/recommended"
],
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts"]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
}
}
},
"rules": {
"@typescript-eslint/explicit-member-accessibility": [
"error",
{ "accessibility": "no-public" }
],
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }],
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"no-console": "error",
"import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package.
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "@/jslib/**/*",
"group": "external",
"position": "after"
},
{
"pattern": "@/src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
],
"rxjs-angular/prefer-takeuntil": "error",
"rxjs/no-exposed-subjects": ["error", { "allowProtected": true }],
"no-restricted-syntax": [
"error",
{
"message": "Calling `svgIcon` directly is not allowed",
"selector": "CallExpression[callee.name='svgIcon']"
},
{
"message": "Accessing FormGroup using `get` is not allowed, use `.value` instead",
"selector": "ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']"
}
],
"curly": ["error", "all"],
"import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway
"no-restricted-imports": ["error", { "patterns": ["src/**/*"] }]
}
},
{
"files": ["*.html"],
"parser": "@angular-eslint/template-parser",
"plugins": ["@angular-eslint/template"],
"rules": {
"@angular-eslint/template/button-has-type": "error"
}
}
]
}

5
.github/CODEOWNERS vendored
View File

@@ -12,8 +12,3 @@
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
# Claude related files
.claude/ @bitwarden/team-ai-sme
.github/workflows/respond.yml @bitwarden/team-ai-sme
.github/workflows/review-code.yml @bitwarden/team-ai-sme

View File

@@ -1,14 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Feature Requests
url: https://community.bitwarden.com/c/feature-requests/
about: Request new features using the Community Forums. Please search existing feature requests before making a new one.
- name: Bitwarden Community Forums
url: https://community.bitwarden.com
about: Please visit the community forums for general community discussion, support and the development roadmap.
- name: Customer Support
url: https://bitwarden.com/contact/
about: Please contact our customer support for account issues and general customer support.
- name: Security Issues
url: https://hackerone.com/bitwarden
about: We use HackerOne to manage security disclosures.

View File

@@ -1,111 +0,0 @@
name: Directory Connector Bug Report
description: File a bug report
title: "[DC] "
labels: ["bug"]
type: bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
- type: textarea
id: reproduce
attributes:
label: Steps To Reproduce
description: How can we reproduce the behavior.
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. Click on '...'
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Result
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Result
description: A clear and concise description of what is happening.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots or Videos
description: If applicable, add screenshots and/or a short video to help explain your problem.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context about the problem here.
- type: dropdown
id: os
attributes:
label: Operating System
description: What operating system(s) are you seeing the problem on?
multiple: true
options:
- Windows
- macOS
- Linux
- Other operating system (please specify in "Additional Context" section)
validations:
required: true
- type: input
id: os-version
attributes:
label: Operating System Version
description: What version of the operating system(s) are you seeing the problem on?
validations:
required: true
- type: dropdown
id: directories
attributes:
label: Directory Service
description: What directory service(s) are you seeing the problem on?
multiple: true
options:
- LDAP - Active Directory
- Another LDAP implementation (please specify in "Additional Context" section)
- Microsoft Entra ID
- Google Workspace
- Okta Universal Directory
- OneLogin
- Other directory service (please specify in "Additional Context" section)
validations:
required: true
- type: dropdown
id: application-type
attributes:
label: Application Type
description: Which Directory Connector application(s) are you seeing the problem on?
multiple: true
options:
- GUI (the desktop application)
- CLI (the bwdc command line application)
validations:
required: true
- type: input
id: version
attributes:
label: Build Version
description: What version of our software are you running?
validations:
required: true
- type: checkboxes
id: issue-tracking-info
attributes:
label: Issue Tracking Info
description: |
Make sure to acknowledge the following before submitting your report!
options:
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
required: true

View File

@@ -9,3 +9,26 @@
## 📸 Screenshots
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
## ⏰ Reminders before review
- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
## 🦮 Reviewer guidelines
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes

View File

@@ -8,6 +8,12 @@
matchManagers: ["github-actions"],
matchUpdateTypes: ["minor", "patch"],
},
{
groupName: "Google Libraries",
matchPackagePatterns: ["google-auth-library", "googleapis"],
matchManagers: ["npm"],
groupSlug: "google-libraries",
},
],
ignoreDeps: [
// yao-pkg is used to create a single executable application bundle for the CLI.
@@ -15,10 +21,5 @@
// This must be manually vetted by our appsec team before upgrading.
// It is excluded from renovate to avoid accidentally upgrading to a non-vetted version.
"@yao-pkg/pkg",
// googleapis uses ESM after 149.0.0 so we are not upgrading it until we have ESM support.
// They release new versions every couple of weeks so ignoring it at the dependency dashboard
// level is not sufficient.
// FIXME: remove and upgrade when we have ESM support.
"googleapis",
],
}

View File

@@ -23,7 +23,7 @@ jobs:
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -51,12 +51,12 @@ jobs:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -111,7 +111,7 @@ jobs:
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
@@ -120,7 +120,7 @@ jobs:
macos-cli:
name: Build Mac CLI
runs-on: macos-15-intel
runs-on: macos-13
needs: setup
permissions:
contents: read
@@ -129,12 +129,12 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -182,7 +182,7 @@ jobs:
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
@@ -200,7 +200,7 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -209,7 +209,7 @@ jobs:
choco install checksum --no-progress
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -258,7 +258,7 @@ jobs:
}
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
@@ -279,12 +279,12 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -338,28 +338,28 @@ jobs:
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: latest.yml
path: ./dist/latest.yml
@@ -379,12 +379,12 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -411,14 +411,14 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -427,7 +427,7 @@ jobs:
macos-gui:
name: Build MacOS GUI
runs-on: macos-15-intel
runs-on: macos-13
needs: setup
permissions:
contents: read
@@ -439,12 +439,12 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -542,28 +542,28 @@ jobs:
CSC_FOR_PULL_REQUEST: true
- name: Upload .zip artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml

View File

@@ -2,36 +2,25 @@ name: Integration Testing
on:
workflow_dispatch:
# Integration tests are slow, so only run them if relevant files have changed.
# This is done at the workflow level and at the job level.
# Make sure these triggers stay consistent with the 'changed-files' job.
push:
branches:
- 'main'
- 'rc'
- "main"
paths:
- ".github/workflows/integration-test.yml" # this file
- "docker-compose.yml" # any change to Docker configuration
- "package.json" # dependencies
- "utils/**" # any change to test fixtures
- "src/services/sync.service.ts" # core sync service used by all directory services
- "src/services/directory-services/ldap-directory.service*" # LDAP directory service
- "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
# Add directory services here as we add test coverage
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
- "./openldap/**/*" # any change to test fixtures
- "./docker-compose.yml" # any change to Docker configuration
- "./package.json" # dependencies
pull_request:
paths:
- ".github/workflows/integration-test.yml" # this file
- "docker-compose.yml" # any change to Docker configuration
- "package.json" # dependencies
- "utils/**" # any change to test fixtures
- "src/services/sync.service.ts" # core sync service used by all directory services
- "src/services/directory-services/ldap-directory.service*" # LDAP directory service
- "src/services/directory-services/gsuite-directory.service*" # Google Workspace directory service
# Add directory services here as we add test coverage
- "src/services/ldap-directory.service*" # we only have integration for LDAP testing at the moment
- "./openldap/**/*" # any change to test fixtures
- "./docker-compose.yml" # any change to Docker configuration
- "./package.json" # dependencies
permissions:
contents: read
checks: write # required by dorny/test-reporter to upload its results
id-token: write # required to use OIDC to login to Azure Key Vault
jobs:
testing:
name: Run tests
@@ -40,7 +29,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -52,7 +41,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -61,88 +50,28 @@ jobs:
- name: Install Node dependencies
run: npm ci
# Get secrets from Azure Key Vault
- name: Azure Login
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get KV Secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-directory-connector
secrets: "GOOGLE-ADMIN-USER,GOOGLE-CLIENT-EMAIL,GOOGLE-DOMAIN,GOOGLE-PRIVATE-KEY"
- name: Azure Logout
uses: bitwarden/gh-actions/azure-logout@main
# Only run relevant tests depending on what files have changed.
# This should be kept consistent with the workflow level triggers.
# Note: docker-compose.yml is only used for ldap for now
- name: Get changed files
id: changed-files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
list-files: shell
token: ${{ secrets.GITHUB_TOKEN }}
# Add directory services here as we add test coverage
filters: |
common:
- '.github/workflows/integration-test.yml'
- 'utils/**'
- 'package.json'
- 'src/services/sync.service.ts'
ldap:
- 'docker-compose.yml'
- 'src/services/directory-services/ldap-directory.service*'
google:
- 'src/services/directory-services/gsuite-directory.service*'
# LDAP
- name: Setup LDAP integration tests
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true'
- name: Install mkcert
run: |
sudo apt-get update
sudo apt-get -y install mkcert
npm run test:integration:setup
- name: Run LDAP integration tests
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.ldap == 'true'
env:
JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing
run: npx jest ldap-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-ldap
- name: Setup integration tests
run: npm run test:integration:setup
# Google Workspace
- name: Run Google Workspace integration tests
if: steps.changed-files.outputs.common == 'true' || steps.changed-files.outputs.google == 'true'
env:
GOOGLE_DOMAIN: ${{ steps.get-kv-secrets.outputs.GOOGLE-DOMAIN }}
GOOGLE_ADMIN_USER: ${{ steps.get-kv-secrets.outputs.GOOGLE-ADMIN-USER }}
GOOGLE_CLIENT_EMAIL: ${{ steps.get-kv-secrets.outputs.GOOGLE-CLIENT-EMAIL }}
GOOGLE_PRIVATE_KEY: ${{ steps.get-kv-secrets.outputs.GOOGLE-PRIVATE-KEY }}
JEST_JUNIT_UNIQUE_OUTPUT_NAME: "true" # avoids junit outputs from clashing
run: |
npx jest gsuite-directory.service.integration.spec.ts --coverage --coverageDirectory=coverage-google
- name: Run integration tests
run: npm run test:integration --coverage
- name: Report test results
id: report
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
# PRs from the repository and all other events are OK.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with:
name: Test Results
path: "junit.xml*"
path: "junit.xml"
reporter: jest-junit
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
- name: Upload results to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2

View File

@@ -26,7 +26,7 @@ jobs:
release_version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -75,7 +75,7 @@ jobs:
- name: Create release
if: ${{ inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
env:
PKG_VERSION: ${{ needs.setup.outputs.release_version }}
with:

View File

@@ -1,28 +0,0 @@
name: Respond
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
permissions: {}
jobs:
respond:
name: Respond
uses: bitwarden/gh-actions/.github/workflows/_respond.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
actions: read
contents: write
id-token: write
issues: write
pull-requests: write

View File

@@ -1,21 +0,0 @@
name: Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
review:
name: Review
uses: bitwarden/gh-actions/.github/workflows/_review-code.yml@main
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
actions: read
contents: read
id-token: write
pull-requests: write

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -34,7 +34,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -53,10 +53,8 @@ jobs:
run: npm run test --coverage
- name: Report test results
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
# PRs from the repository and all other events are OK.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
with:
name: Test Results
path: "junit.xml"
@@ -64,9 +62,7 @@ jobs:
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
- name: Upload results to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2

View File

@@ -42,15 +42,14 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-contents: write
- name: Checkout Branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true

7
.gitignore vendored
View File

@@ -2,9 +2,6 @@
.DS_Store
Thumbs.db
# Environment variables used for tests
.env
# IDEs and editors
.idea/
.project
@@ -33,8 +30,8 @@ build-cli
.angular/cache
# Testing
coverage*
junit.xml*
coverage
junit.xml
# Misc
*.crx

View File

@@ -18,17 +18,15 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": {
"base": "dist"
},
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.json",
"assets": [],
"styles": [],
"scripts": [],
"browser": "src/main.ts"
"scripts": []
}
}
}

View File

@@ -11,8 +11,8 @@ services:
- LDAP_TLS_KEY_FILE=/certs/openldap-key.pem
- LDAP_TLS_CA_FILE=/certs/rootCA.pem
volumes:
- "./utils/openldap/ldifs:/ldifs"
- "./utils/openldap/certs:/certs"
- "./openldap/ldifs:/ldifs"
- "./openldap/certs:/certs"
ports:
- "1389:1389"
- "1636:1636"

View File

@@ -4,13 +4,13 @@
},
"productName": "Bitwarden Directory Connector",
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
"app": "build"
},
"afterSign": "scripts/notarize.mjs",
"afterSign": "scripts/notarize.js",
"mac": {
"artifactName": "Bitwarden-Connector-${version}-mac.${ext}",
"category": "public.app-category.productivity",
@@ -22,7 +22,7 @@
},
"win": {
"target": ["portable", "nsis"],
"sign": "scripts/sign.mjs"
"sign": "scripts/sign.js"
},
"linux": {
"category": "Utility",

View File

@@ -1,149 +0,0 @@
// @ts-check
import eslint from "@eslint/js";
import tsParser from "@typescript-eslint/parser";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import prettierConfig from "eslint-config-prettier";
import importPlugin from "eslint-plugin-import";
import rxjsX from "eslint-plugin-rxjs-x";
import rxjsAngularX from "eslint-plugin-rxjs-angular-x";
import angularEslint from "@angular-eslint/eslint-plugin-template";
import angularParser from "@angular-eslint/template-parser";
import globals from "globals";
export default [
// Global ignores (replaces .eslintignore)
{
ignores: [
"dist/**",
"dist-cli/**",
"build/**",
"build-cli/**",
"coverage/**",
"**/*.cjs",
"eslint.config.mjs",
"scripts/**/*.js",
"**/node_modules/**",
],
},
// Base config for all JavaScript/TypeScript files
{
files: ["**/*.ts", "**/*.js"],
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
parser: tsParser,
parserOptions: {
project: ["./tsconfig.eslint.json"],
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
"@typescript-eslint": tsPlugin,
import: importPlugin,
"rxjs-x": rxjsX,
"rxjs-angular-x": rxjsAngularX,
},
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
rules: {
// ESLint recommended rules
...eslint.configs.recommended.rules,
// TypeScript ESLint recommended rules
...tsPlugin.configs.recommended.rules,
// Import plugin recommended rules
...importPlugin.flatConfigs.recommended.rules,
// RxJS recommended rules
...rxjsX.configs.recommended.rules,
// Custom project rules
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
"@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
"@typescript-eslint/no-this-alias": ["error", { allowedNames: ["self"] }],
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
"no-console": "error",
"import/no-unresolved": "off", // TODO: Look into turning on once each package is an actual package.
"import/order": [
"error",
{
alphabetize: {
order: "asc",
},
"newlines-between": "always",
pathGroups: [
{
pattern: "@/jslib/**/*",
group: "external",
position: "after",
},
{
pattern: "@/src/**/*",
group: "parent",
position: "before",
},
],
pathGroupsExcludedImportTypes: ["builtin"],
},
],
"rxjs-angular-x/prefer-takeuntil": "error",
"rxjs-x/no-exposed-subjects": ["error", { allowProtected: true }],
"no-restricted-syntax": [
"error",
{
message: "Calling `svgIcon` directly is not allowed",
selector: "CallExpression[callee.name='svgIcon']",
},
{
message: "Accessing FormGroup using `get` is not allowed, use `.value` instead",
selector:
"ChainExpression[expression.object.callee.property.name='get'][expression.property.name='value']",
},
],
curly: ["error", "all"],
"import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway
"no-restricted-imports": ["error", { patterns: ["src/**/*"] }],
},
},
// Jest test files (includes any test-related files)
{
files: ["**/*.spec.ts", "**/test.setup.ts", "**/spec/**/*.ts", "**/utils/**/*fixtures*.ts"],
languageOptions: {
globals: {
...globals.jest,
},
},
},
// Angular HTML templates
{
files: ["**/*.html"],
languageOptions: {
parser: angularParser,
},
plugins: {
"@angular-eslint/template": angularEslint,
},
rules: {
"@angular-eslint/template/button-has-type": "error",
},
},
// Prettier config (must be last to override other configs)
prettierConfig,
];

View File

@@ -1,14 +1,14 @@
import { pathsToModuleNameMapper } from "ts-jest";
import tsconfig from "./tsconfig.json" with { type: "json" };
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
import angularPresetsModule from "jest-preset-angular/presets/index.js";
const { defaultTransformerOptions } = angularPresetsModule;
const { compilerOptions } = tsconfig;
const tsPreset = require("ts-jest/jest-preset");
const angularPreset = require("jest-preset-angular/jest-preset");
const { defaultTransformerOptions } = require("jest-preset-angular/presets");
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
module.exports = {
// ...tsPreset,
// ...angularPreset,
preset: "jest-preset-angular",
reporters: ["default", "jest-junit"],
@@ -26,6 +26,7 @@ export default {
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
// Workaround for a memory leak that crashes tests in CI:
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
// Also anecdotally improves performance when run locally

View File

@@ -1,745 +0,0 @@
# Plan: Remove StateService and jslib Dependencies
## Context
Directory Connector currently depends on StateService from jslib, which is a massive pre-StateProvider monolith containing 200+ getter/setter methods for all Bitwarden clients. This creates significant maintenance burden and blocks deletion of unused jslib code.
**Current State (Phase 1 Complete):**
- ✅ StateServiceVNext has been implemented with a flat key-value structure
- ✅ Migration service handles transition from old account-based structure to new flat structure
- ⚠️ Both old and new StateService implementations coexist during migration
- ❌ Three jslib services still depend on old StateService: TokenService, CryptoService, EnvironmentService
- ❌ Two Electron components depend on old StateService: WindowMain, TrayMain
**Problem:**
The old StateService cannot be removed until all dependencies are eliminated. Analysis reveals:
- **TokenService**: Used for API authentication (9/32 methods actually used)
- **CryptoService**: Completely unused by DC (0/61 methods used) - carried over from monolith
- **EnvironmentService**: Used for custom server URLs (4/11 methods used)
- **WindowMain/TrayMain**: Used for Electron window/tray state persistence (6 methods total)
**Goal:**
Replace jslib services with simplified DC-specific implementations that use StateServiceVNext, enabling complete removal of old StateService and unlocking Phase 2 (jslib code cleanup).
**User Decisions:**
1. ✅ Create simplified DC-specific versions of Token/Environment services (clean break from jslib)
2. ✅ Keep WindowMain/TrayMain as-is (minimize scope, focus on StateService removal)
3. ✅ Automatic migration on first launch (transparent to users)
## Critical Files
### Files to Create (New Implementations)
- `src/services/token/token.service.ts` - DC-specific token service
- `src/abstractions/token.service.ts` - Token service interface
- `src/services/environment/environment.service.ts` - DC-specific environment service
- `src/abstractions/environment.service.ts` - Environment service interface
- `src/utils/jwt.util.ts` - JWT decoding utility (no dependencies)
### Files to Modify (Update Dependencies)
- `src/services/api.service.ts` - Switch from jslib TokenService to DC TokenService
- `src/services/auth.service.ts` - Update EnvironmentService import
- `src/services/sync.service.ts` - Update EnvironmentService import
- `src/commands/config.command.ts` - Update EnvironmentService import
- `src/bwdc.ts` - Remove old StateService, instantiate new services
- `src/main.ts` - Remove old StateService, instantiate new services
- `src/app/services/services.module.ts` - Remove old StateService, provide new services
- `src/app/app.component.ts` - Update TokenService import
- `jslib/electron/src/window.main.ts` - Adapt to use StateServiceVNext
- `jslib/electron/src/tray.main.ts` - Adapt to use StateServiceVNext
### Files to Delete (After Migration)
- `jslib/common/src/services/token.service.ts` - jslib TokenService
- `jslib/common/src/abstractions/token.service.ts` - jslib TokenService interface
- `jslib/common/src/services/crypto.service.ts` - Unused CryptoService
- `jslib/common/src/abstractions/crypto.service.ts` - Unused CryptoService interface
- `jslib/common/src/services/environment.service.ts` - jslib EnvironmentService
- `jslib/common/src/abstractions/environment.service.ts` - jslib EnvironmentService interface
- `src/services/state-service/state.service.ts` - Old DC StateService
- `src/abstractions/state.service.ts` - Old DC StateService interface
- `jslib/common/src/services/state.service.ts` - Old jslib StateService
- `jslib/common/src/abstractions/state.service.ts` - Old jslib StateService interface
## Implementation Plan
### Step 1: Create JWT Utility (No Dependencies)
Create `src/utils/jwt.util.ts` with standalone JWT decoding function:
```typescript
export interface DecodedToken {
exp: number;
iat: number;
nbf: number;
sub: string; // user ID
client_id?: string;
[key: string]: any;
}
export function decodeJwt(token: string): DecodedToken {
// Validate JWT structure (3 parts: header.payload.signature)
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT format");
}
// Decode payload (base64url to JSON)
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
return JSON.parse(atob(payload));
}
export function getTokenExpirationDate(token: string): Date | null {
const decoded = decodeJwt(token);
if (!decoded.exp) return null;
return new Date(decoded.exp * 1000);
}
export function tokenSecondsRemaining(token: string, offsetSeconds = 0): number {
const expDate = getTokenExpirationDate(token);
if (!expDate) return 0;
const msRemaining = expDate.getTime() - Date.now() - offsetSeconds * 1000;
return Math.floor(msRemaining / 1000);
}
export function tokenNeedsRefresh(token: string, minutesBeforeExpiration = 5): boolean {
const secondsRemaining = tokenSecondsRemaining(token);
return secondsRemaining < minutesBeforeExpiration * 60;
}
```
**Why:** Standalone utility avoids service dependencies, can be tested independently, reusable.
### Step 2: Create DC TokenService
Create `src/abstractions/token.service.ts`:
```typescript
export interface TokenService {
// Token storage
setTokens(
accessToken: string,
refreshToken: string,
clientIdClientSecret?: [string, string],
): Promise<void>;
getToken(): Promise<string | null>;
getRefreshToken(): Promise<string | null>;
clearToken(): Promise<void>;
// API key authentication
getClientId(): Promise<string | null>;
getClientSecret(): Promise<string | null>;
// Two-factor token (rarely used)
getTwoFactorToken(): Promise<string | null>;
clearTwoFactorToken(): Promise<void>;
// Token validation (delegates to jwt.util)
decodeToken(token?: string): Promise<DecodedToken | null>;
tokenNeedsRefresh(minutesBeforeExpiration?: number): Promise<boolean>;
}
```
Create `src/services/token/token.service.ts`:
```typescript
import { StateServiceVNext } from "@/abstractions/state-vNext.service";
import { SecureStorageService } from "@/jslib/common/src/abstractions/storage.service";
import { TokenService as ITokenService } from "@/abstractions/token.service";
import {
decodeJwt,
tokenNeedsRefresh as checkTokenNeedsRefresh,
DecodedToken,
} from "@/utils/jwt.util";
export class TokenService implements ITokenService {
// Storage keys
private TOKEN_KEY = "accessToken";
private REFRESH_TOKEN_KEY = "refreshToken";
private CLIENT_ID_KEY = "apiKeyClientId";
private CLIENT_SECRET_KEY = "apiKeyClientSecret";
private TWO_FACTOR_TOKEN_KEY = "twoFactorToken";
constructor(
private stateService: StateServiceVNext,
private secureStorageService: SecureStorageService,
) {}
async setTokens(
accessToken: string,
refreshToken: string,
clientIdClientSecret?: [string, string],
): Promise<void> {
await this.secureStorageService.save(this.TOKEN_KEY, accessToken);
await this.secureStorageService.save(this.REFRESH_TOKEN_KEY, refreshToken);
if (clientIdClientSecret) {
await this.secureStorageService.save(this.CLIENT_ID_KEY, clientIdClientSecret[0]);
await this.secureStorageService.save(this.CLIENT_SECRET_KEY, clientIdClientSecret[1]);
}
}
async getToken(): Promise<string | null> {
return await this.secureStorageService.get<string>(this.TOKEN_KEY);
}
async getRefreshToken(): Promise<string | null> {
return await this.secureStorageService.get<string>(this.REFRESH_TOKEN_KEY);
}
async clearToken(): Promise<void> {
await this.secureStorageService.remove(this.TOKEN_KEY);
await this.secureStorageService.remove(this.REFRESH_TOKEN_KEY);
await this.secureStorageService.remove(this.CLIENT_ID_KEY);
await this.secureStorageService.remove(this.CLIENT_SECRET_KEY);
}
async getClientId(): Promise<string | null> {
return await this.secureStorageService.get<string>(this.CLIENT_ID_KEY);
}
async getClientSecret(): Promise<string | null> {
return await this.secureStorageService.get<string>(this.CLIENT_SECRET_KEY);
}
async getTwoFactorToken(): Promise<string | null> {
return await this.secureStorageService.get<string>(this.TWO_FACTOR_TOKEN_KEY);
}
async clearTwoFactorToken(): Promise<void> {
await this.secureStorageService.remove(this.TWO_FACTOR_TOKEN_KEY);
}
async decodeToken(token?: string): Promise<DecodedToken | null> {
const tokenToUse = token ?? (await this.getToken());
if (!tokenToUse) return null;
try {
return decodeJwt(tokenToUse);
} catch {
return null;
}
}
async tokenNeedsRefresh(minutesBeforeExpiration = 5): Promise<boolean> {
const token = await this.getToken();
if (!token) return true;
try {
return checkTokenNeedsRefresh(token, minutesBeforeExpiration);
} catch {
return true;
}
}
}
```
Create `src/services/token/token.service.spec.ts` with comprehensive tests covering:
- Token storage/retrieval
- Token clearing
- JWT decoding
- Token expiration logic
- Error handling for malformed tokens
### Step 3: Create DC EnvironmentService
Create `src/abstractions/environment.service.ts`:
```typescript
export interface EnvironmentUrls {
base?: string;
api?: string;
identity?: string;
webVault?: string;
icons?: string;
notifications?: string;
events?: string;
keyConnector?: string;
}
export interface EnvironmentService {
setUrls(urls: EnvironmentUrls): Promise<void>;
setUrlsFromStorage(): Promise<void>;
hasBaseUrl(): boolean;
getApiUrl(): string;
getIdentityUrl(): string;
getWebVaultUrl(): string;
getIconsUrl(): string;
getNotificationsUrl(): string;
getEventsUrl(): string;
getKeyConnectorUrl(): string;
}
```
Create `src/services/environment/environment.service.ts`:
```typescript
import { StateServiceVNext } from "@/abstractions/state-vNext.service";
import {
EnvironmentService as IEnvironmentService,
EnvironmentUrls,
} from "@/abstractions/environment.service";
export class EnvironmentService implements IEnvironmentService {
private readonly DEFAULT_URLS = {
api: "https://api.bitwarden.com",
identity: "https://identity.bitwarden.com",
webVault: "https://vault.bitwarden.com",
icons: "https://icons.bitwarden.net",
notifications: "https://notifications.bitwarden.com",
events: "https://events.bitwarden.com",
};
private urls: EnvironmentUrls = {};
constructor(private stateService: StateServiceVNext) {}
async setUrls(urls: EnvironmentUrls): Promise<void> {
// Normalize URLs: trim whitespace, remove trailing slashes, add https:// if missing
const normalized: EnvironmentUrls = {};
for (const [key, value] of Object.entries(urls)) {
if (!value) continue;
let url = value.trim();
url = url.replace(/\/+$/, ""); // Remove trailing slashes
if (!/^https?:\/\//i.test(url)) {
url = `https://${url}`;
}
normalized[key] = url;
}
this.urls = normalized;
await this.stateService.setEnvironmentUrls(normalized);
}
async setUrlsFromStorage(): Promise<void> {
const stored = await this.stateService.getEnvironmentUrls();
this.urls = stored ?? {};
}
hasBaseUrl(): boolean {
return !!this.urls.base;
}
getApiUrl(): string {
return this.urls.api ?? this.urls.base + "/api" ?? this.DEFAULT_URLS.api;
}
getIdentityUrl(): string {
return this.urls.identity ?? this.urls.base + "/identity" ?? this.DEFAULT_URLS.identity;
}
getWebVaultUrl(): string {
return this.urls.webVault ?? this.urls.base ?? this.DEFAULT_URLS.webVault;
}
getIconsUrl(): string {
return this.urls.icons ?? this.urls.base + "/icons" ?? this.DEFAULT_URLS.icons;
}
getNotificationsUrl(): string {
return (
this.urls.notifications ??
this.urls.base + "/notifications" ??
this.DEFAULT_URLS.notifications
);
}
getEventsUrl(): string {
return this.urls.events ?? this.urls.base + "/events" ?? this.DEFAULT_URLS.events;
}
getKeyConnectorUrl(): string {
return this.urls.keyConnector ?? "";
}
}
```
Create `src/services/environment/environment.service.spec.ts` with tests covering:
- URL normalization (trailing slashes, https prefix)
- Storage persistence
- Default URL fallbacks
- Custom URL override
- Base URL derivation
### Step 4: Add Environment URL Storage to StateServiceVNext
Update `src/models/state.model.ts` to add environment URL storage key:
```typescript
export const StorageKeysVNext = {
// ... existing keys ...
environmentUrls: "environmentUrls",
};
```
Update `src/abstractions/state-vNext.service.ts` to add methods:
```typescript
export interface StateServiceVNext {
// ... existing methods ...
getEnvironmentUrls(): Promise<EnvironmentUrls | null>;
setEnvironmentUrls(urls: EnvironmentUrls): Promise<void>;
}
```
Update `src/services/state-service/state-vNext.service.ts` implementation to add storage methods.
### Step 5: Update StateMigrationService for Token/Environment Data
Update `src/services/state-service/stateMigration.service.ts` to migrate:
**Token data (from secure storage):**
- `accessToken``accessToken` (same key, no change needed)
- `refreshToken``refreshToken` (same key, no change needed)
- `apiKeyClientId``apiKeyClientId` (same key, no change needed)
- `apiKeyClientSecret``apiKeyClientSecret` (same key, no change needed)
**Environment URLs (from account state):**
- `environmentUrls` from account → `environmentUrls` in flat structure
Add migration test cases to `src/services/state-service/stateMigration.service.spec.ts`.
### Step 6: Remove CryptoService Dependencies
Since CryptoService is completely unused by DC:
1. Search for all imports of `CryptoService` in `src/` code
2. Remove all instantiations and injections
3. Verify no methods are actually called
4. Remove from DI containers (services.module.ts, bwdc.ts, main.ts)
Expected: Zero usage, straightforward removal.
### Step 7: Update WindowMain/TrayMain to Use StateServiceVNext
Update `jslib/electron/src/window.main.ts`:
```typescript
// Change constructor to accept StateServiceVNext instead of StateService
constructor(
private stateService: StateServiceVNext, // Changed from StateService
// ... other params
) {}
// Update method calls to use StateServiceVNext interface
async getWindowSettings(): Promise<any> {
return await this.stateService.getWindowSettings();
}
async setWindowSettings(settings: any): Promise<void> {
await this.stateService.setWindowSettings(settings);
}
```
Update `jslib/electron/src/tray.main.ts` similarly:
```typescript
constructor(
private stateService: StateServiceVNext, // Changed from StateService
// ... other params
) {}
// Update method calls
async getEnableTray(): Promise<boolean> {
return await this.stateService.getEnableTray();
}
// ... etc for other tray settings
```
**Required:** Add window/tray setting storage to StateServiceVNext:
- `getWindowSettings()` / `setWindowSettings()`
- `getEnableTray()` / `getEnableMinimizeToTray()` / `getEnableCloseToTray()` / `getAlwaysShowDock()`
### Step 8: Update Service Registrations
**In `src/app/services/services.module.ts`:**
```typescript
// Remove old services
- import { StateService } from '@/services/state-service/state.service';
- import { TokenService } from '@/jslib/common/src/services/token.service';
- import { CryptoService } from '@/jslib/common/src/services/crypto.service';
- import { EnvironmentService } from '@/jslib/common/src/services/environment.service';
// Add new services
+ import { TokenService } from '@/services/token/token.service';
+ import { EnvironmentService } from '@/services/environment/environment.service';
providers: [
// Remove old StateService provider
- { provide: StateService, useClass: StateService },
// Add new service providers
+ { provide: TokenService, useClass: TokenService },
+ { provide: EnvironmentService, useClass: EnvironmentService },
// Keep StateServiceVNext
{ provide: StateServiceVNext, useClass: StateServiceVNextImplementation },
]
```
**In `src/bwdc.ts` (CLI):**
```typescript
// Remove old service instantiations
- this.stateService = new StateService(/* ... */);
- this.cryptoService = new CryptoService(/* ... */);
- this.tokenService = new TokenService(/* ... */);
- this.environmentService = new EnvironmentService(this.stateService);
// Add new service instantiations
+ this.tokenService = new TokenService(this.stateServiceVNext, secureStorageService);
+ this.environmentService = new EnvironmentService(this.stateServiceVNext);
```
**In `src/main.ts` (Electron):**
```typescript
// Remove old service instantiations
- this.stateService = new StateService(/* ... */);
- this.cryptoService = new CryptoService(/* ... */);
- this.tokenService = new TokenService(/* ... */);
- this.environmentService = new EnvironmentService(this.stateService);
// Add new service instantiations
+ this.tokenService = new TokenService(this.stateServiceVNext, secureStorageService);
+ this.environmentService = new EnvironmentService(this.stateServiceVNext);
// Update WindowMain/TrayMain to use StateServiceVNext
- this.windowMain = new WindowMain(this.stateService, /* ... */);
- this.trayMain = new TrayMain(this.stateService, /* ... */);
+ this.windowMain = new WindowMain(this.stateServiceVNext, /* ... */);
+ this.trayMain = new TrayMain(this.stateServiceVNext, /* ... */);
```
### Step 9: Update Import Statements
Update all files that import Token/Environment services:
**Files to update:**
- `src/services/api.service.ts` - Change TokenService import to DC version
- `src/services/auth.service.ts` - Change EnvironmentService import to DC version
- `src/services/sync.service.ts` - Change EnvironmentService import to DC version
- `src/commands/config.command.ts` - Change EnvironmentService import to DC version
- `src/app/app.component.ts` - Change TokenService import to DC version
**Pattern:**
```typescript
// Before
import { TokenService } from "@/jslib/common/src/services/token.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
// After
import { TokenService } from "@/abstractions/token.service";
import { EnvironmentService } from "@/abstractions/environment.service";
```
### Step 10: Delete Old StateService and jslib Services
**Delete these files (after all references removed):**
```bash
# Old StateService implementations
src/services/state-service/state.service.ts
src/abstractions/state.service.ts
jslib/common/src/services/state.service.ts
jslib/common/src/abstractions/state.service.ts
# jslib Token/Crypto/Environment services
jslib/common/src/services/token.service.ts
jslib/common/src/abstractions/token.service.ts
jslib/common/src/services/crypto.service.ts
jslib/common/src/abstractions/crypto.service.ts
jslib/common/src/services/environment.service.ts
jslib/common/src/abstractions/environment.service.ts
```
**Rename StateServiceVNext to StateService:**
```bash
# Rename files
mv src/services/state-service/state-vNext.service.ts src/services/state-service/state.service.ts
mv src/services/state-service/state-vNext.service.spec.ts src/services/state-service/state.service.spec.ts
mv src/abstractions/state-vNext.service.ts src/abstractions/state.service.ts
# Update all imports from StateServiceVNext to StateService
# Find and replace: StateServiceVNext → StateService
```
### Step 11: Update Tests
**Update existing tests that mock StateService:**
- Update mocks to use new StateService interface (flat key-value structure)
- Remove mocks for Token/Crypto/Environment services where they inject old versions
- Add mocks for new DC Token/Environment services
**Add new test files:**
- `src/services/token/token.service.spec.ts` (created in Step 2)
- `src/services/environment/environment.service.spec.ts` (created in Step 3)
- `src/utils/jwt.util.spec.ts` (JWT utility tests)
**Update integration tests:**
- Verify token storage/retrieval works correctly
- Verify environment URL configuration persists
- Verify window/tray settings persist in Electron app
## Verification Plan
### Unit Tests
```bash
npm test # Run all unit tests
```
**Expected:**
- All new service tests pass (TokenService, EnvironmentService, JWT util)
- All existing tests pass with updated mocks
- No test failures due to StateService removal
### Integration Tests
```bash
npm run test:integration
```
**Expected:**
- LDAP sync tests pass
- Authentication flow works correctly
- Configuration persistence works
### Manual Testing - CLI
```bash
# Build and run CLI
npm run build:cli:watch
node ./build-cli/bwdc.js --help
# Test authentication
node ./build-cli/bwdc.js config server https://vault.bitwarden.com
node ./build-cli/bwdc.js login --apikey
# Test sync
node ./build-cli/bwdc.js config directory ldap
node ./build-cli/bwdc.js config ldap.hostname ldap.example.com
node ./build-cli/bwdc.js sync
# Test logout
node ./build-cli/bwdc.js logout
```
**Verify:**
- ✅ Server URL configuration persists
- ✅ Login stores tokens correctly
- ✅ Token refresh works automatically
- ✅ Sync completes successfully
- ✅ Logout clears tokens
### Manual Testing - Desktop App
```bash
# Build and run desktop app
npm run electron
```
**Verify:**
- ✅ Window position/size persists across restarts
- ✅ "Always on top" setting persists
- ✅ Tray icon shows/hides based on settings
- ✅ Minimize/close to tray works
- ✅ Login/logout flow works
- ✅ Sync functionality works
- ✅ Custom server URL configuration works
### Migration Testing
**Test migration from existing installation:**
1. Install current production version
2. Configure directory connection and run sync
3. Install new version with StateService removal
4. Launch app - verify automatic migration occurs
5. Verify all settings preserved:
- Directory configuration
- Organization ID
- Server URLs
- Window/tray settings
- Authentication tokens
**Expected:**
- ✅ Migration runs automatically on first launch
- ✅ All user data preserved
- ✅ No user action required
- ✅ App functions identically to before
### Regression Testing
Run through all major workflows:
1. **Configuration**: Set up each directory type (LDAP, Entra, Google, Okta, OneLogin)
2. **Authentication**: Login with API key, verify token refresh
3. **Sync**: Full sync, incremental sync (delta tokens), detect changes via hash
4. **Custom server**: Configure self-hosted Bitwarden server
5. **Electron features**: Window management, tray behavior
**Expected:** No regressions in functionality.
## Rollback Plan
If critical issues discovered post-deployment:
1. **Revert commit** removing StateService
2. **Keep StateServiceVNext in parallel** (already coexisting)
3. **Debug issues** in development
4. **Re-attempt removal** after fixes
**Risk Assessment:** Low - StateServiceVNext has been in production since Phase 1 PR merge, proven stable.
## Success Criteria
- ✅ All old StateService implementations deleted
- ✅ StateServiceVNext renamed to StateService (becomes primary)
- ✅ jslib TokenService, CryptoService, EnvironmentService deleted
- ✅ DC-specific Token/Environment services implemented and tested
- ✅ All unit tests pass
- ✅ All integration tests pass
- ✅ Manual testing shows no regressions
- ✅ Migration from old state structure works automatically
- ✅ WindowMain/TrayMain adapted to new StateService
- ✅ Zero references to old StateService in codebase
## Next Steps (After Completion)
This unblocks **Phase 2: Remove Remaining jslib Code**:
- Delete unused jslib models (AccountData, AccountSettings, etc.)
- Delete unused jslib services that referenced StateService
- Clean up jslib/common folder of unused client code
- Potentially merge remaining jslib code into src/ (flatten structure)
Estimated effort: 2-3 days for experienced developer familiar with codebase.

View File

@@ -0,0 +1,28 @@
import { webcrypto } from "crypto";
import "jest-preset-angular/setup-jest";
Object.defineProperty(window, "CSS", { value: null });
Object.defineProperty(window, "getComputedStyle", {
value: () => {
return {
display: "none",
appearance: ["-webkit-appearance"],
};
},
});
Object.defineProperty(document, "doctype", {
value: "<!DOCTYPE html>",
});
Object.defineProperty(document.body.style, "transform", {
value: () => {
return {
enumerable: true,
configurable: true,
};
},
});
Object.defineProperty(window, "crypto", {
value: webcrypto,
});

View File

@@ -0,0 +1,63 @@
import { Directive, EventEmitter, Output } from "@angular/core";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
@Directive()
export class EnvironmentComponent {
@Output() onSaved = new EventEmitter();
iconsUrl: string;
identityUrl: string;
apiUrl: string;
webVaultUrl: string;
notificationsUrl: string;
baseUrl: string;
showCustom = false;
constructor(
protected platformUtilsService: PlatformUtilsService,
protected environmentService: EnvironmentService,
protected i18nService: I18nService,
) {
const urls = this.environmentService.getUrls();
this.baseUrl = urls.base || "";
this.webVaultUrl = urls.webVault || "";
this.apiUrl = urls.api || "";
this.identityUrl = urls.identity || "";
this.iconsUrl = urls.icons || "";
this.notificationsUrl = urls.notifications || "";
}
async submit() {
const resUrls = await this.environmentService.setUrls({
base: this.baseUrl,
api: this.apiUrl,
identity: this.identityUrl,
webVault: this.webVaultUrl,
icons: this.iconsUrl,
notifications: this.notificationsUrl,
});
// re-set urls since service can change them, ex: prefixing https://
this.baseUrl = resUrls.base;
this.apiUrl = resUrls.api;
this.identityUrl = resUrls.identity;
this.webVaultUrl = resUrls.webVault;
this.iconsUrl = resUrls.icons;
this.notificationsUrl = resUrls.notifications;
this.platformUtilsService.showToast("success", null, this.i18nService.t("environmentSaved"));
this.saved();
}
toggleCustom() {
this.showCustom = !this.showCustom;
}
protected saved() {
this.onSaved.emit();
}
}

View File

@@ -1,4 +1,4 @@
import { InjectOptions, Injector, ProviderToken } from "@angular/core";
import { InjectFlags, InjectOptions, Injector, ProviderToken } from "@angular/core";
export class ModalInjector implements Injector {
constructor(
@@ -12,7 +12,8 @@ export class ModalInjector implements Injector {
options: InjectOptions & { optional?: false },
): T;
get<T>(token: ProviderToken<T>, notFoundValue: null, options: InjectOptions): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, options?: InjectOptions | InjectFlags): T;
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
get(token: any, notFoundValue?: any): any;
get(token: any, notFoundValue?: any, flags?: any): any {
return this._additionalTokens.get(token) ?? this._parentInjector.get<any>(token, notFoundValue);

View File

@@ -1,4 +1,5 @@
import { lastValueFrom, Observable, Subject } from "rxjs";
import { Observable, Subject } from "rxjs";
import { first } from "rxjs/operators";
export class ModalRef {
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
@@ -44,6 +45,6 @@ export class ModalRef {
}
onClosedPromise(): Promise<any> {
return lastValueFrom(this.onClosed);
return this.onClosed.pipe(first()).toPromise();
}
}

View File

@@ -1,77 +1,75 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { CommonModule } from "@angular/common";
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } from "ngx-toastr";
import {
DefaultNoComponentGlobalConfig,
GlobalConfig,
Toast as BaseToast,
ToastPackage,
ToastrService,
TOAST_CONFIG,
} from "ngx-toastr";
@Component({
selector: "[toast-component2]",
template: `
@if (options().closeButton) {
<button (click)="remove()" type="button" class="toast-close-button" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
}
<button
*ngIf="options.closeButton"
(click)="remove()"
type="button"
class="toast-close-button"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
<div class="icon">
<i></i>
</div>
<div>
@if (title()) {
<div [class]="options().titleClass" [attr.aria-label]="title()">
{{ title() }}
@if (duplicatesCount) {
[{{ duplicatesCount + 1 }}]
}
</div>
}
@if (message() && options().enableHtml) {
<div
role="alertdialog"
aria-live="polite"
[class]="options().messageClass"
[innerHTML]="message()"
></div>
}
@if (message() && !options().enableHtml) {
<div
role="alertdialog"
aria-live="polite"
[class]="options().messageClass"
[attr.aria-label]="message()"
>
{{ message() }}
</div>
}
</div>
@if (options().progressBar) {
<div>
<div class="toast-progress" [style.width]="width + '%'"></div>
<div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title">
{{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container>
</div>
}
`,
styles: `
:host {
&.toast-in {
animation: toast-animation var(--animation-duration) var(--animation-easing);
}
&.toast-out {
animation: toast-animation var(--animation-duration) var(--animation-easing) reverse
forwards;
}
}
@keyframes toast-animation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
<div
*ngIf="message && options.enableHtml"
role="alertdialog"
aria-live="polite"
[class]="options.messageClass"
[innerHTML]="message"
></div>
<div
*ngIf="message && !options.enableHtml"
role="alertdialog"
aria-live="polite"
[class]="options.messageClass"
[attr.aria-label]="message"
>
{{ message }}
</div>
</div>
<div *ngIf="options.progressBar">
<div class="toast-progress" [style.width]="width + '%'"></div>
</div>
`,
animations: [
trigger("flyInOut", [
state("inactive", style({ opacity: 0 })),
state("active", style({ opacity: 1 })),
state("removed", style({ opacity: 0 })),
transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")),
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
]),
],
preserveWhitespaces: false,
standalone: false,
})
export class BitwardenToast extends Toast {}
export class BitwardenToast extends BaseToast {
constructor(
protected toastrService: ToastrService,
public toastPackage: ToastPackage,
) {
super(toastrService, toastPackage);
}
}
export const BitwardenToastGlobalConfig: GlobalConfig = {
...DefaultNoComponentGlobalConfig,

View File

@@ -1,5 +1,5 @@
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
import { take } from "rxjs";
import { take } from "rxjs/operators";
import { Utils } from "@/jslib/common/src/misc/utils";

View File

@@ -0,0 +1,143 @@
import { LOCALE_ID, NgModule } from "@angular/core";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { Account } from "@/jslib/common/src/models/domain/account";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { ApiService } from "@/jslib/common/src/services/api.service";
import { AppIdService } from "@/jslib/common/src/services/appId.service";
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { StateService } from "@/jslib/common/src/services/state.service";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
import { TokenService } from "@/jslib/common/src/services/token.service";
import {
SafeInjectionToken,
SECURE_STORAGE,
WINDOW,
} from "../../../../src/app/services/injection-tokens";
import { SafeProvider, safeProvider } from "../../../../src/app/services/safe-provider";
import { BroadcasterService } from "./broadcaster.service";
import { ModalService } from "./modal.service";
import { ValidationService } from "./validation.service";
@NgModule({
declarations: [],
providers: [
safeProvider({ provide: WINDOW, useValue: window }),
safeProvider({
provide: LOCALE_ID as SafeInjectionToken<string>,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction],
}),
safeProvider(ValidationService),
safeProvider(ModalService),
safeProvider({
provide: AppIdServiceAbstraction,
useClass: AppIdService,
deps: [StorageServiceAbstraction],
}),
safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
safeProvider({
provide: EnvironmentServiceAbstraction,
useClass: EnvironmentService,
deps: [StateServiceAbstraction],
}),
safeProvider({
provide: TokenServiceAbstraction,
useClass: TokenService,
deps: [StateServiceAbstraction],
}),
safeProvider({
provide: CryptoServiceAbstraction,
useClass: CryptoService,
deps: [
CryptoFunctionServiceAbstraction,
PlatformUtilsServiceAbstraction,
LogService,
StateServiceAbstraction,
],
}),
safeProvider({
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
appIdService: AppIdServiceAbstraction,
) =>
new ApiService(
tokenService,
platformUtilsService,
environmentService,
appIdService,
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
),
deps: [
TokenServiceAbstraction,
PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
AppIdServiceAbstraction,
],
}),
safeProvider({
provide: BroadcasterServiceAbstraction,
useClass: BroadcasterService,
useAngularDecorators: true,
}),
safeProvider({
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
logService: LogService,
stateMigrationService: StateMigrationServiceAbstraction,
) =>
new StateService(
storageService,
secureStorageService,
logService,
stateMigrationService,
new StateFactory(GlobalState, Account),
),
deps: [
StorageServiceAbstraction,
SECURE_STORAGE,
LogService,
StateMigrationServiceAbstraction,
],
}),
safeProvider({
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
secureStorageService: StorageServiceAbstraction,
) =>
new StateMigrationService(
storageService,
secureStorageService,
new StateFactory(GlobalState, Account),
),
deps: [StorageServiceAbstraction, SECURE_STORAGE],
}),
] satisfies SafeProvider[],
})
export class JslibServicesModule {}

View File

@@ -9,7 +9,7 @@ import {
Type,
ViewContainerRef,
} from "@angular/core";
import { first, firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { DynamicModalComponent } from "../components/modal/dynamic-modal.component";
import { ModalInjector } from "../components/modal/modal-injector";
@@ -58,7 +58,7 @@ export class ModalService {
viewContainerRef.insert(modalComponentRef.hostView);
await firstValueFrom(modalRef.onCreated);
await modalRef.onCreated.pipe(first()).toPromise();
return [modalRef, modalComponentRef.instance.componentRef.instance];
}

View File

@@ -0,0 +1,195 @@
import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { EncryptionType } from "@/jslib/common/src/enums/encryptionType";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { ContainerService } from "@/jslib/common/src/services/container.service";
describe("EncString", () => {
afterEach(() => {
(window as any).bitwardenContainerService = undefined;
});
describe("Rsa2048_OaepSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("3.data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
it("invalid", () => {
const encString = new EncString("3.data|test");
expect(encString).toEqual({
encryptedString: "3.data|test",
encryptionType: 3,
});
});
});
describe("decrypt", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).resolves("decrypted");
beforeEach(() => {
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
});
it("decrypts correctly", async () => {
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("decrypted");
});
it("result should be cached", async () => {
const decrypted = await encString.decrypt(null);
cryptoService.received(1).decryptToUtf8(Arg.any(), Arg.any());
expect(decrypted).toBe("decrypted");
});
});
});
describe("AesCbc256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("0.iv|data");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
it("invalid", () => {
const encString = new EncString("0.iv|data|mac");
expect(encString).toEqual({
encryptedString: "0.iv|data|mac",
encryptionType: 0,
});
});
});
});
describe("AesCbc256_HmacSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("valid", () => {
const encString = new EncString("2.iv|data|mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("invalid", () => {
const encString = new EncString("2.iv|data");
expect(encString).toEqual({
encryptedString: "2.iv|data",
encryptionType: 2,
});
});
});
it("Exit early if null", () => {
const encString = new EncString(null);
expect(encString).toEqual({
encryptedString: null,
});
});
describe("decrypt", () => {
it("throws exception when bitwarden container not initialized", async () => {
const encString = new EncString(null);
expect.assertions(1);
try {
await encString.decrypt(null);
} catch (e) {
expect(e.message).toEqual("global bitwardenContainerService not initialized.");
}
});
it("handles value it can't decrypt", async () => {
const encString = new EncString(null);
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).throws("error");
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("[error: cannot decrypt]");
expect(encString).toEqual({
decryptedValue: "[error: cannot decrypt]",
encryptedString: null,
});
});
it("passes along key", async () => {
const encString = new EncString(null);
const key = Substitute.for<SymmetricCryptoKey>();
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
await encString.decrypt(null, key);
cryptoService.received().decryptToUtf8(encString, key);
});
});
});

View File

@@ -0,0 +1,69 @@
import { EncryptionType } from "@/jslib/common/src/enums/encryptionType";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { makeStaticByteArray } from "../utils";
describe("SymmetricCryptoKey", () => {
it("errors if no key", () => {
const t = () => {
new SymmetricCryptoKey(null);
};
expect(t).toThrowError("Must provide key");
});
describe("guesses encKey from key length", () => {
it("AesCbc256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key,
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 0,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: null,
});
});
it("AesCbc128_HmacSha256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key, EncryptionType.AesCbc128_HmacSha256_B64);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 16),
encKeyB64: "AAECAwQFBgcICQoLDA0ODw==",
encType: 1,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: key.slice(16, 32),
macKeyB64: "EBESExQVFhcYGRobHB0eHw==",
});
});
it("AesCbc256_HmacSha256_B64", () => {
const key = makeStaticByteArray(64);
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 32),
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 2,
key: key,
keyB64:
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
macKey: key.slice(32, 64),
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
});
});
it("unknown length", () => {
const t = () => {
new SymmetricCryptoKey(makeStaticByteArray(30));
};
expect(t).toThrowError("Unable to determine encType.");
});
});
});

View File

@@ -0,0 +1,127 @@
import { sequentialize } from "@/jslib/common/src/misc/sequentialize";
describe("sequentialize decorator", () => {
it("should call the function once", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
});
it("should call the function once for each instance of the object", async () => {
const foo = new Foo();
const foo2 = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
promises.push(foo2.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
expect(foo2.calls).toBe(1);
});
it("should call the function once with key function", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.baz(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(1);
});
it("should call the function again when already resolved", async () => {
const foo = new Foo();
await foo.bar(1);
expect(foo.calls).toBe(1);
await foo.bar(1);
expect(foo.calls).toBe(2);
});
it("should call the function again when already resolved with a key function", async () => {
const foo = new Foo();
await foo.baz(1);
expect(foo.calls).toBe(1);
await foo.baz(1);
expect(foo.calls).toBe(2);
});
it("should call the function for each argument", async () => {
const foo = new Foo();
await Promise.all([foo.bar(1), foo.bar(1), foo.bar(2), foo.bar(2), foo.bar(3), foo.bar(3)]);
expect(foo.calls).toBe(3);
});
it("should call the function for each argument with key function", async () => {
const foo = new Foo();
await Promise.all([foo.baz(1), foo.baz(1), foo.baz(2), foo.baz(2), foo.baz(3), foo.baz(3)]);
expect(foo.calls).toBe(3);
});
it("should return correct result for each call", async () => {
const foo = new Foo();
const allRes: number[] = [];
await Promise.all([
foo.bar(1).then((res) => allRes.push(res)),
foo.bar(1).then((res) => allRes.push(res)),
foo.bar(2).then((res) => allRes.push(res)),
foo.bar(2).then((res) => allRes.push(res)),
foo.bar(3).then((res) => allRes.push(res)),
foo.bar(3).then((res) => allRes.push(res)),
]);
expect(foo.calls).toBe(3);
expect(allRes.length).toBe(6);
allRes.sort();
expect(allRes).toEqual([2, 2, 4, 4, 6, 6]);
});
it("should return correct result for each call with key function", async () => {
const foo = new Foo();
const allRes: number[] = [];
await Promise.all([
foo.baz(1).then((res) => allRes.push(res)),
foo.baz(1).then((res) => allRes.push(res)),
foo.baz(2).then((res) => allRes.push(res)),
foo.baz(2).then((res) => allRes.push(res)),
foo.baz(3).then((res) => allRes.push(res)),
foo.baz(3).then((res) => allRes.push(res)),
]);
expect(foo.calls).toBe(3);
expect(allRes.length).toBe(6);
allRes.sort();
expect(allRes).toEqual([3, 3, 6, 6, 9, 9]);
});
});
class Foo {
calls = 0;
@sequentialize((args) => "bar" + args[0])
bar(a: number): Promise<number> {
this.calls++;
return new Promise((res) => {
setTimeout(() => {
res(a * 2);
}, Math.random() * 100);
});
}
@sequentialize((args) => "baz" + args[0])
baz(a: number): Promise<number> {
this.calls++;
return new Promise((res) => {
setTimeout(() => {
res(a * 3);
}, Math.random() * 100);
});
}
}

View File

@@ -0,0 +1,110 @@
import { sequentialize } from "@/jslib/common/src/misc/sequentialize";
import { throttle } from "@/jslib/common/src/misc/throttle";
describe("throttle decorator", () => {
it("should call the function once at a time", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
});
it("should call the function once at a time for each object", async () => {
const foo = new Foo();
const foo2 = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.bar(1));
promises.push(foo2.bar(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
expect(foo2.calls).toBe(10);
});
it("should call the function limit at a time", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.baz(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
});
it("should call the function limit at a time for each object", async () => {
const foo = new Foo();
const foo2 = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.baz(1));
promises.push(foo2.baz(1));
}
await Promise.all(promises);
expect(foo.calls).toBe(10);
expect(foo2.calls).toBe(10);
});
it("should work together with sequentialize", async () => {
const foo = new Foo();
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(foo.qux(Math.floor(i / 2) * 2));
}
await Promise.all(promises);
expect(foo.calls).toBe(5);
});
});
class Foo {
calls = 0;
inflight = 0;
@throttle(1, () => "bar")
bar(a: number) {
this.calls++;
this.inflight++;
return new Promise((res) => {
setTimeout(() => {
expect(this.inflight).toBe(1);
this.inflight--;
res(a * 2);
}, Math.random() * 10);
});
}
@throttle(5, () => "baz")
baz(a: number) {
this.calls++;
this.inflight++;
return new Promise((res) => {
setTimeout(() => {
expect(this.inflight).toBeLessThanOrEqual(5);
this.inflight--;
res(a * 3);
}, Math.random() * 10);
});
}
@sequentialize((args) => "qux" + args[0])
@throttle(1, () => "qux")
qux(a: number) {
this.calls++;
this.inflight++;
return new Promise((res) => {
setTimeout(() => {
expect(this.inflight).toBe(1);
this.inflight--;
res(a * 3);
}, Math.random() * 10);
});
}
}

View File

@@ -0,0 +1,102 @@
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
const originalConsole = console;
let caughtMessage: any;
declare let console: any;
export function interceptConsole(interceptions: any): object {
console = {
log: function () {
// eslint-disable-next-line
interceptions.log = arguments;
},
warn: function () {
// eslint-disable-next-line
interceptions.warn = arguments;
},
error: function () {
// eslint-disable-next-line
interceptions.error = arguments;
},
};
return interceptions;
}
export function restoreConsole() {
console = originalConsole;
}
describe("ConsoleLogService", () => {
let logService: ConsoleLogService;
beforeEach(() => {
caughtMessage = {};
interceptConsole(caughtMessage);
logService = new ConsoleLogService(true);
});
afterAll(() => {
restoreConsole();
});
it("filters messages below the set threshold", () => {
logService = new ConsoleLogService(true, () => true);
logService.debug("debug");
logService.info("info");
logService.warning("warning");
logService.error("error");
expect(caughtMessage).toEqual({});
});
it("only writes debug messages in dev mode", () => {
logService = new ConsoleLogService(false);
logService.debug("debug message");
expect(caughtMessage.log).toBeUndefined();
});
it("writes debug/info messages to console.log", () => {
logService.debug("this is a debug message");
expect(caughtMessage).toMatchObject({
log: { "0": "this is a debug message" },
});
logService.info("this is an info message");
expect(caughtMessage).toMatchObject({
log: { "0": "this is an info message" },
});
});
it("writes warning messages to console.warn", () => {
logService.warning("this is a warning message");
expect(caughtMessage).toMatchObject({
warn: { 0: "this is a warning message" },
});
});
it("writes error messages to console.error", () => {
logService.error("this is an error message");
expect(caughtMessage).toMatchObject({
error: { 0: "this is an error message" },
});
});
it("times with output to info", async () => {
logService.time();
await new Promise((r) => setTimeout(r, 250));
const duration = logService.timeEnd();
expect(duration[0]).toBe(0);
expect(duration[1]).toBeGreaterThan(0);
expect(duration[1]).toBeLessThan(500 * 10e6);
expect(caughtMessage).toEqual(expect.arrayContaining([]));
expect(caughtMessage.log.length).toBe(1);
expect(caughtMessage.log[0]).toEqual(expect.stringMatching(/^default: \d+\.?\d*ms$/));
});
it("filters time output", async () => {
logService = new ConsoleLogService(true, () => true);
logService.time();
logService.timeEnd();
expect(caughtMessage).toEqual({});
});
});

View File

@@ -0,0 +1,84 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { Account } from "@/jslib/common/src/models/domain/account";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
const userId = "USER_ID";
describe("State Migration Service", () => {
let storageService: SubstituteOf<StorageService>;
let secureStorageService: SubstituteOf<StorageService>;
let stateFactory: SubstituteOf<StateFactory>;
let stateMigrationService: StateMigrationService;
beforeEach(() => {
storageService = Substitute.for<StorageService>();
secureStorageService = Substitute.for<StorageService>();
stateFactory = Substitute.for<StateFactory>();
stateMigrationService = new StateMigrationService(
storageService,
secureStorageService,
stateFactory,
);
});
describe("StateVersion 3 to 4 migration", async () => {
beforeEach(() => {
const globalVersion3: Partial<GlobalState> = {
stateVersion: StateVersion.Three,
};
storageService.get("global", Arg.any()).resolves(globalVersion3);
storageService.get("authenticatedAccounts", Arg.any()).resolves([userId]);
});
it("clears everBeenUnlocked", async () => {
const accountVersion3: Account = {
profile: {
apiKeyClientId: null,
convertAccountToKeyConnector: null,
email: "EMAIL",
emailVerified: true,
everBeenUnlocked: true,
hasPremiumPersonally: false,
kdfIterations: 100000,
kdfType: 0,
keyHash: "KEY_HASH",
lastSync: "LAST_SYNC",
userId: userId,
usesKeyConnector: false,
forcePasswordReset: false,
},
};
const expectedAccountVersion4: Account = {
profile: {
...accountVersion3.profile,
},
};
delete expectedAccountVersion4.profile.everBeenUnlocked;
storageService.get(userId, Arg.any()).resolves(accountVersion3);
await stateMigrationService.migrate();
storageService.received(1).save(userId, expectedAccountVersion4, Arg.any());
});
it("updates StateVersion number", async () => {
await stateMigrationService.migrate();
storageService.received(1).save(
"global",
Arg.is((globals: GlobalState) => globals.stateVersion === StateVersion.Four),
Arg.any(),
);
});
});
});

View File

@@ -0,0 +1,5 @@
import { webcrypto } from "crypto";
Object.defineProperty(window, "crypto", {
value: webcrypto,
});

View File

@@ -1,3 +1,7 @@
import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { EncString } from "@/jslib/common/src/models/domain/encString";
function newGuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
@@ -17,10 +21,17 @@ export function BuildTestObject<T, K extends keyof T = keyof T>(
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
}
export function mockEnc(s: string): EncString {
const mock = Substitute.for<EncString>();
mock.decrypt(Arg.any(), Arg.any()).resolves(s);
return mock;
}
export function makeStaticByteArray(length: number, start = 0) {
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = start + i;
}
return arr.buffer;
return arr;
}

View File

@@ -0,0 +1,86 @@
import { HashPurpose } from "../enums/hashPurpose";
import { KdfType } from "../enums/kdfType";
import { KeySuffixOptions } from "../enums/keySuffixOptions";
import { EncArrayBuffer } from "../models/domain/encArrayBuffer";
import { EncString } from "../models/domain/encString";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { ProfileOrganizationResponse } from "../models/response/profileOrganizationResponse";
import { ProfileProviderOrganizationResponse } from "../models/response/profileProviderOrganizationResponse";
import { ProfileProviderResponse } from "../models/response/profileProviderResponse";
export abstract class CryptoService {
setKey: (key: SymmetricCryptoKey) => Promise<any>;
setKeyHash: (keyHash: string) => Promise<void>;
setEncKey: (encKey: string) => Promise<void>;
setEncPrivateKey: (encPrivateKey: string) => Promise<void>;
setOrgKeys: (
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
) => Promise<void>;
setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise<void>;
getKey: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
getKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise<SymmetricCryptoKey>;
getKeyHash: () => Promise<string>;
compareAndUpdateKeyHash: (masterPassword: string, key: SymmetricCryptoKey) => Promise<boolean>;
getEncKey: (key?: SymmetricCryptoKey) => Promise<SymmetricCryptoKey>;
getPublicKey: () => Promise<ArrayBuffer>;
getPrivateKey: () => Promise<ArrayBuffer>;
getFingerprint: (userId: string, publicKey?: ArrayBuffer) => Promise<string[]>;
getOrgKeys: () => Promise<Map<string, SymmetricCryptoKey>>;
getOrgKey: (orgId: string) => Promise<SymmetricCryptoKey>;
getProviderKey: (providerId: string) => Promise<SymmetricCryptoKey>;
hasKey: () => Promise<boolean>;
hasKeyInMemory: (userId?: string) => Promise<boolean>;
hasKeyStored: (keySuffix?: KeySuffixOptions, userId?: string) => Promise<boolean>;
hasEncKey: () => Promise<boolean>;
clearKey: (clearSecretStorage?: boolean, userId?: string) => Promise<any>;
clearKeyHash: () => Promise<any>;
clearEncKey: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise<any>;
clearProviderKeys: (memoryOnly?: boolean) => Promise<any>;
clearPinProtectedKey: () => Promise<any>;
clearKeys: (userId?: string) => Promise<any>;
toggleKey: () => Promise<any>;
makeKey: (
password: string,
salt: string,
kdf: KdfType,
kdfIterations: number,
) => Promise<SymmetricCryptoKey>;
makeKeyFromPin: (
pin: string,
salt: string,
kdf: KdfType,
kdfIterations: number,
protectedKeyCs?: EncString,
) => Promise<SymmetricCryptoKey>;
makeShareKey: () => Promise<[EncString, SymmetricCryptoKey]>;
makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>;
makePinKey: (
pin: string,
salt: string,
kdf: KdfType,
kdfIterations: number,
) => Promise<SymmetricCryptoKey>;
makeSendKey: (keyMaterial: ArrayBuffer) => Promise<SymmetricCryptoKey>;
hashPassword: (
password: string,
key: SymmetricCryptoKey,
hashPurpose?: HashPurpose,
) => Promise<string>;
makeEncKey: (key: SymmetricCryptoKey) => Promise<[SymmetricCryptoKey, EncString]>;
remakeEncKey: (
key: SymmetricCryptoKey,
encKey?: SymmetricCryptoKey,
) => Promise<[SymmetricCryptoKey, EncString]>;
encrypt: (plainValue: string | ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncString>;
encryptToBytes: (plainValue: ArrayBuffer, key?: SymmetricCryptoKey) => Promise<EncArrayBuffer>;
rsaEncrypt: (data: ArrayBuffer, publicKey?: ArrayBuffer) => Promise<EncString>;
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise<ArrayBuffer>;
decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise<string>;
decryptFromBytes: (encBuf: ArrayBuffer, key: SymmetricCryptoKey) => Promise<ArrayBuffer>;
randomNumber: (min: number, max: number) => Promise<number>;
validateKey: (key: SymmetricCryptoKey) => Promise<boolean>;
}

View File

@@ -1,2 +1,34 @@
// Stub file - re-exports DC EnvironmentService
export { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service";
import { Observable } from "rxjs";
export type Urls = {
base?: string;
webVault?: string;
api?: string;
identity?: string;
icons?: string;
notifications?: string;
events?: string;
keyConnector?: string;
};
export type PayPalConfig = {
businessId?: string;
buttonAction?: string;
};
export abstract class EnvironmentService {
urls: Observable<Urls>;
hasBaseUrl: () => boolean;
getNotificationsUrl: () => string;
getWebVaultUrl: () => string;
getSendUrl: () => string;
getIconsUrl: () => string;
getApiUrl: () => string;
getIdentityUrl: () => string;
getEventsUrl: () => string;
getKeyConnectorUrl: () => string;
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
getUrls: () => Urls;
}

View File

@@ -1,2 +1,218 @@
// Stub file - re-exports DC StateService
export { StateService } from "@/src/abstractions/state.service";
import { Observable } from "rxjs";
import { KdfType } from "../enums/kdfType";
import { ThemeType } from "../enums/themeType";
import { UriMatchType } from "../enums/uriMatchType";
import { OrganizationData } from "../models/data/organizationData";
import { ProviderData } from "../models/data/providerData";
import { Account } from "../models/domain/account";
import { EncString } from "../models/domain/encString";
import { EnvironmentUrls } from "../models/domain/environmentUrls";
import { StorageOptions } from "../models/domain/storageOptions";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { WindowState } from "../models/domain/windowState";
export abstract class StateService<T extends Account = Account> {
accounts$: Observable<{ [userId: string]: T }>;
activeAccount$: Observable<string>;
addAccount: (account: T) => Promise<void>;
setActiveUser: (userId: string) => Promise<void>;
clean: (options?: StorageOptions) => Promise<void>;
init: () => Promise<void>;
getAccessToken: (options?: StorageOptions) => Promise<string>;
setAccessToken: (value: string, options?: StorageOptions) => Promise<void>;
getAddEditCipherInfo: (options?: StorageOptions) => Promise<any>;
setAddEditCipherInfo: (value: any, options?: StorageOptions) => Promise<void>;
getAlwaysShowDock: (options?: StorageOptions) => Promise<boolean>;
setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise<void>;
getApiKeyClientId: (options?: StorageOptions) => Promise<string>;
setApiKeyClientId: (value: string, options?: StorageOptions) => Promise<void>;
getApiKeyClientSecret: (options?: StorageOptions) => Promise<string>;
setApiKeyClientSecret: (value: string, options?: StorageOptions) => Promise<void>;
getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise<boolean>;
setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricAwaitingAcceptance: (options?: StorageOptions) => Promise<boolean>;
setBiometricAwaitingAcceptance: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricFingerprintValidated: (options?: StorageOptions) => Promise<boolean>;
setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricLocked: (options?: StorageOptions) => Promise<boolean>;
setBiometricLocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getBiometricText: (options?: StorageOptions) => Promise<string>;
setBiometricText: (value: string, options?: StorageOptions) => Promise<void>;
getBiometricUnlock: (options?: StorageOptions) => Promise<boolean>;
setBiometricUnlock: (value: boolean, options?: StorageOptions) => Promise<void>;
getCanAccessPremium: (options?: StorageOptions) => Promise<boolean>;
getClearClipboard: (options?: StorageOptions) => Promise<number>;
setClearClipboard: (value: number, options?: StorageOptions) => Promise<void>;
getCollapsedGroupings: (options?: StorageOptions) => Promise<string[]>;
setCollapsedGroupings: (value: string[], options?: StorageOptions) => Promise<void>;
getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise<boolean>;
setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
getCryptoMasterKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setCryptoMasterKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise<string>;
setCryptoMasterKeyAuto: (value: string, options?: StorageOptions) => Promise<void>;
getCryptoMasterKeyB64: (options?: StorageOptions) => Promise<string>;
setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise<void>;
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
getDecodedToken: (options?: StorageOptions) => Promise<any>;
setDecodedToken: (value: any, options?: StorageOptions) => Promise<void>;
getDecryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setDecryptedCryptoSymmetricKey: (
value: SymmetricCryptoKey,
options?: StorageOptions,
) => Promise<void>;
getDecryptedOrganizationKeys: (
options?: StorageOptions,
) => Promise<Map<string, SymmetricCryptoKey>>;
setDecryptedOrganizationKeys: (
value: Map<string, SymmetricCryptoKey>,
options?: StorageOptions,
) => Promise<void>;
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
getDecryptedPrivateKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
setDecryptedPrivateKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
getDecryptedProviderKeys: (options?: StorageOptions) => Promise<Map<string, SymmetricCryptoKey>>;
setDecryptedProviderKeys: (
value: Map<string, SymmetricCryptoKey>,
options?: StorageOptions,
) => Promise<void>;
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
getDisableAutoBiometricsPrompt: (options?: StorageOptions) => Promise<boolean>;
setDisableAutoBiometricsPrompt: (value: boolean, options?: StorageOptions) => Promise<void>;
getDisableAutoTotpCopy: (options?: StorageOptions) => Promise<boolean>;
setDisableAutoTotpCopy: (value: boolean, options?: StorageOptions) => Promise<void>;
getDisableBadgeCounter: (options?: StorageOptions) => Promise<boolean>;
setDisableBadgeCounter: (value: boolean, options?: StorageOptions) => Promise<void>;
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
getDisableGa: (options?: StorageOptions) => Promise<boolean>;
setDisableGa: (value: boolean, options?: StorageOptions) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>;
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
setEmailVerified: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableAlwaysOnTop: (options?: StorageOptions) => Promise<boolean>;
setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableBiometric: (options?: StorageOptions) => Promise<boolean>;
setEnableBiometric: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableMinimizeToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableMinimizeToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableStartToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableStartToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableTray: (options?: StorageOptions) => Promise<boolean>;
setEnableTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise<string>;
setEncryptedCryptoSymmetricKey: (value: string, options?: StorageOptions) => Promise<void>;
getEncryptedOrganizationKeys: (options?: StorageOptions) => Promise<any>;
setEncryptedOrganizationKeys: (
value: Map<string, SymmetricCryptoKey>,
options?: StorageOptions,
) => Promise<void>;
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
getEncryptedPrivateKey: (options?: StorageOptions) => Promise<string>;
setEncryptedPrivateKey: (value: string, options?: StorageOptions) => Promise<void>;
getEncryptedProviderKeys: (options?: StorageOptions) => Promise<any>;
setEncryptedProviderKeys: (value: any, options?: StorageOptions) => Promise<void>;
getEntityId: (options?: StorageOptions) => Promise<string>;
getEnvironmentUrls: (options?: StorageOptions) => Promise<EnvironmentUrls>;
setEnvironmentUrls: (value: EnvironmentUrls, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<any>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getForcePasswordReset: (options?: StorageOptions) => Promise<boolean>;
setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise<void>;
getInstalledVersion: (options?: StorageOptions) => Promise<string>;
setInstalledVersion: (value: string, options?: StorageOptions) => Promise<void>;
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
getKdfIterations: (options?: StorageOptions) => Promise<number>;
setKdfIterations: (value: number, options?: StorageOptions) => Promise<void>;
getKdfType: (options?: StorageOptions) => Promise<KdfType>;
setKdfType: (value: KdfType, options?: StorageOptions) => Promise<void>;
getKeyHash: (options?: StorageOptions) => Promise<string>;
setKeyHash: (value: string, options?: StorageOptions) => Promise<void>;
getLastActive: (options?: StorageOptions) => Promise<number>;
setLastActive: (value: number, options?: StorageOptions) => Promise<void>;
getLastSync: (options?: StorageOptions) => Promise<string>;
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
getLegacyEtmKey: (options?: StorageOptions) => Promise<SymmetricCryptoKey>;
setLegacyEtmKey: (value: SymmetricCryptoKey, options?: StorageOptions) => Promise<void>;
getLocalData: (options?: StorageOptions) => Promise<any>;
setLocalData: (value: string, options?: StorageOptions) => Promise<void>;
getLocale: (options?: StorageOptions) => Promise<string>;
setLocale: (value: string, options?: StorageOptions) => Promise<void>;
getLoginRedirect: (options?: StorageOptions) => Promise<any>;
setLoginRedirect: (value: any, options?: StorageOptions) => Promise<void>;
getMainWindowSize: (options?: StorageOptions) => Promise<number>;
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>;
setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise<void>;
getNoAutoPromptBiometrics: (options?: StorageOptions) => Promise<boolean>;
setNoAutoPromptBiometrics: (value: boolean, options?: StorageOptions) => Promise<void>;
getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise<string>;
setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise<void>;
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>;
setOrganizations: (
value: { [id: string]: OrganizationData },
options?: StorageOptions,
) => Promise<void>;
getPasswordGenerationOptions: (options?: StorageOptions) => Promise<any>;
setPasswordGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
getUsernameGenerationOptions: (options?: StorageOptions) => Promise<any>;
setUsernameGenerationOptions: (value: any, options?: StorageOptions) => Promise<void>;
getGeneratorOptions: (options?: StorageOptions) => Promise<any>;
setGeneratorOptions: (value: any, options?: StorageOptions) => Promise<void>;
getProtectedPin: (options?: StorageOptions) => Promise<string>;
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
getProviders: (options?: StorageOptions) => Promise<{ [id: string]: ProviderData }>;
setProviders: (value: { [id: string]: ProviderData }, options?: StorageOptions) => Promise<void>;
getPublicKey: (options?: StorageOptions) => Promise<ArrayBuffer>;
setPublicKey: (value: ArrayBuffer, options?: StorageOptions) => Promise<void>;
getRefreshToken: (options?: StorageOptions) => Promise<string>;
setRefreshToken: (value: string, options?: StorageOptions) => Promise<void>;
getRememberedEmail: (options?: StorageOptions) => Promise<string>;
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
getSettings: (options?: StorageOptions) => Promise<any>;
setSettings: (value: string, options?: StorageOptions) => Promise<void>;
getSsoCodeVerifier: (options?: StorageOptions) => Promise<string>;
setSsoCodeVerifier: (value: string, options?: StorageOptions) => Promise<void>;
getSsoOrgIdentifier: (options?: StorageOptions) => Promise<string>;
setSsoOrganizationIdentifier: (value: string, options?: StorageOptions) => Promise<void>;
getSsoState: (options?: StorageOptions) => Promise<string>;
setSsoState: (value: string, options?: StorageOptions) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
getUserId: (options?: StorageOptions) => Promise<string>;
getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>;
setUsesKeyConnector: (vaule: boolean, options?: StorageOptions) => Promise<void>;
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
getStateVersion: () => Promise<number>;
setStateVersion: (value: number) => Promise<void>;
getWindow: () => Promise<WindowState>;
setWindow: (value: WindowState) => Promise<void>;
}

View File

@@ -0,0 +1,4 @@
export abstract class StateMigrationService {
needsMigration: () => Promise<boolean>;
migrate: () => Promise<void>;
}

View File

@@ -1,2 +1,32 @@
// Stub file - re-exports DC TokenService
export { TokenService } from "@/src/abstractions/token.service";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
export abstract class TokenService {
setTokens: (
accessToken: string,
refreshToken: string,
clientIdClientSecret: [string, string],
) => Promise<any>;
setToken: (token: string) => Promise<any>;
getToken: () => Promise<string>;
setRefreshToken: (refreshToken: string) => Promise<any>;
getRefreshToken: () => Promise<string>;
setClientId: (clientId: string) => Promise<any>;
getClientId: () => Promise<string>;
setClientSecret: (clientSecret: string) => Promise<any>;
getClientSecret: () => Promise<string>;
setTwoFactorToken: (tokenResponse: IdentityTokenResponse) => Promise<any>;
getTwoFactorToken: () => Promise<string>;
clearTwoFactorToken: () => Promise<any>;
clearToken: (userId?: string) => Promise<any>;
decodeToken: (token?: string) => any;
getTokenExpirationDate: () => Promise<Date>;
tokenSecondsRemaining: (offsetSeconds?: number) => Promise<number>;
tokenNeedsRefresh: (minutes?: number) => Promise<boolean>;
getUserId: () => Promise<string>;
getEmail: () => Promise<string>;
getEmailVerified: () => Promise<boolean>;
getName: () => Promise<string>;
getPremium: () => Promise<boolean>;
getIssuer: () => Promise<string>;
getIsExternal: () => Promise<boolean>;
}

View File

@@ -0,0 +1,6 @@
export enum AuthenticationStatus {
Locked = "locked",
Unlocked = "unlocked",
LoggedOut = "loggedOut",
Active = "active",
}

View File

@@ -0,0 +1,4 @@
export enum CipherRepromptType {
None = 0,
Password = 1,
}

View File

@@ -0,0 +1,6 @@
export enum CipherType {
Login = 1,
SecureNote = 2,
Card = 3,
Identity = 4,
}

View File

@@ -0,0 +1,7 @@
export enum EmergencyAccessStatusType {
Invited = 0,
Accepted = 1,
Confirmed = 2,
RecoveryInitiated = 3,
RecoveryApproved = 4,
}

View File

@@ -0,0 +1,4 @@
export enum EmergencyAccessType {
View = 0,
Takeover = 1,
}

View File

@@ -0,0 +1,6 @@
export enum FieldType {
Text = 0,
Hidden = 1,
Boolean = 2,
Linked = 3,
}

View File

@@ -0,0 +1,4 @@
export enum FileUploadType {
Direct = 0,
Azure = 1,
}

View File

@@ -0,0 +1,4 @@
export enum HashPurpose {
ServerAuthorization = 1,
LocalAuthorization = 2,
}

View File

@@ -0,0 +1,4 @@
export enum KeySuffixOptions {
Auto = "auto",
Biometric = "biometric",
}

View File

@@ -0,0 +1,20 @@
export enum NotificationType {
SyncCipherUpdate = 0,
SyncCipherCreate = 1,
SyncLoginDelete = 2,
SyncFolderDelete = 3,
SyncCiphers = 4,
SyncVault = 5,
SyncOrgKeys = 6,
SyncFolderCreate = 7,
SyncFolderUpdate = 8,
SyncCipherDelete = 9,
SyncSettings = 10,
LogOut = 11,
SyncSendCreate = 12,
SyncSendUpdate = 13,
SyncSendDelete = 14,
}

View File

@@ -0,0 +1,5 @@
export enum OrganizationUserStatusType {
Invited = 0,
Accepted = 1,
Confirmed = 2,
}

View File

@@ -0,0 +1,7 @@
export enum OrganizationUserType {
Owner = 0,
Admin = 1,
User = 2,
Manager = 3,
Custom = 4,
}

View File

@@ -0,0 +1,11 @@
export enum PaymentMethodType {
Card = 0,
BankAccount = 1,
PayPal = 2,
BitPay = 3,
Credit = 4,
WireTransfer = 5,
AppleInApp = 6,
GoogleInApp = 7,
Check = 8,
}

View File

@@ -0,0 +1,27 @@
export enum Permissions {
AccessEventLogs,
AccessImportExport,
AccessReports,
/**
* @deprecated Sep 29 2021: This permission has been split out to `createNewCollections`, `editAnyCollection`, and
* `deleteAnyCollection`. It exists here for backwards compatibility with Server versions <= 1.43.0
*/
ManageAllCollections,
/**
* @deprecated Sep 29 2021: This permission has been split out to `editAssignedCollections` and
* `deleteAssignedCollections`. It exists here for backwards compatibility with Server versions <= 1.43.0
*/
ManageAssignedCollections,
ManageGroups,
ManageOrganization,
ManagePolicies,
ManageProvider,
ManageUsers,
ManageUsersPassword,
CreateNewCollections,
EditAnyCollection,
DeleteAnyCollection,
EditAssignedCollections,
DeleteAssignedCollections,
ManageSso,
}

View File

@@ -0,0 +1,3 @@
export enum PlanSponsorshipType {
FamiliesForEnterprise = 0,
}

View File

@@ -0,0 +1,14 @@
export enum PlanType {
Free = 0,
FamiliesAnnually2019 = 1,
TeamsMonthly2019 = 2,
TeamsAnnually2019 = 3,
EnterpriseMonthly2019 = 4,
EnterpriseAnnually2019 = 5,
Custom = 6,
FamiliesAnnually = 7,
TeamsMonthly = 8,
TeamsAnnually = 9,
EnterpriseMonthly = 10,
EnterpriseAnnually = 11,
}

View File

@@ -0,0 +1,6 @@
export enum ProductType {
Free = 0,
Families = 1,
Teams = 2,
Enterprise = 3,
}

View File

@@ -0,0 +1,5 @@
export enum ProviderUserStatusType {
Invited = 0,
Accepted = 1,
Confirmed = 2,
}

View File

@@ -0,0 +1,4 @@
export enum ProviderUserType {
ProviderAdmin = 0,
ServiceUser = 1,
}

View File

@@ -0,0 +1,33 @@
export enum SsoType {
None = 0,
OpenIdConnect = 1,
Saml2 = 2,
}
export enum OpenIdConnectRedirectBehavior {
RedirectGet = 0,
FormPost = 1,
}
export enum Saml2BindingType {
HttpRedirect = 1,
HttpPost = 2,
}
export enum Saml2NameIdFormat {
NotConfigured = 0,
Unspecified = 1,
EmailAddress = 2,
X509SubjectName = 3,
WindowsDomainQualifiedName = 4,
KerberosPrincipalName = 5,
EntityIdentifier = 6,
Persistent = 7,
Transient = 8,
}
export enum Saml2SigningBehavior {
IfIdpWantAuthnRequestsSigned = 0,
Always = 1,
Never = 3,
}

View File

@@ -3,6 +3,5 @@ export enum StateVersion {
Two = 2, // Move to a typed State object
Three = 3, // Fix migration of users' premium status
Four = 4, // Fix 'Never Lock' option by removing stale data
Five = 5, // New state service implementation
Latest = Five,
Latest = Four,
}

View File

@@ -0,0 +1,7 @@
export enum TransactionType {
Charge = 0,
Credit = 1,
PromotionalCredit = 2,
ReferralCredit = 3,
Refund = 4,
}

View File

@@ -0,0 +1,8 @@
export enum UriMatchType {
Domain = 0,
Host = 1,
StartsWith = 2,
Exact = 3,
RegularExpression = 4,
Never = 5,
}

View File

@@ -0,0 +1,13 @@
import { Account } from "../models/domain/account";
export class AccountFactory<T extends Account = Account> {
private accountConstructor: new (init: Partial<T>) => T;
constructor(accountConstructor: new (init: Partial<T>) => T) {
this.accountConstructor = accountConstructor;
}
create(args: Partial<T>) {
return new this.accountConstructor(args);
}
}

View File

@@ -0,0 +1,13 @@
import { GlobalState } from "../models/domain/globalState";
export class GlobalStateFactory<T extends GlobalState = GlobalState> {
private globalStateConstructor: new (init: Partial<T>) => T;
constructor(globalStateConstructor: new (init: Partial<T>) => T) {
this.globalStateConstructor = globalStateConstructor;
}
create(args?: Partial<T>) {
return new this.globalStateConstructor(args);
}
}

View File

@@ -0,0 +1,29 @@
import { Account } from "../models/domain/account";
import { GlobalState } from "../models/domain/globalState";
import { AccountFactory } from "./accountFactory";
import { GlobalStateFactory } from "./globalStateFactory";
export class StateFactory<
TGlobal extends GlobalState = GlobalState,
TAccount extends Account = Account,
> {
private globalStateFactory: GlobalStateFactory<TGlobal>;
private accountFactory: AccountFactory<TAccount>;
constructor(
globalStateConstructor: new (init: Partial<TGlobal>) => TGlobal,
accountConstructor: new (init: Partial<TAccount>) => TAccount,
) {
this.globalStateFactory = new GlobalStateFactory(globalStateConstructor);
this.accountFactory = new AccountFactory(accountConstructor);
}
createGlobal(args: Partial<TGlobal>): TGlobal {
return this.globalStateFactory.create(args);
}
createAccount(args: Partial<TAccount>): TAccount {
return this.accountFactory.create(args);
}
}

View File

@@ -26,4 +26,9 @@ export class NodeUtils {
.on("error", (err) => reject(err));
});
}
// https://stackoverflow.com/a/31394257
static bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
}
}

View File

@@ -0,0 +1,72 @@
import { ITreeNodeObject, TreeNode } from "../models/domain/treeNode";
export class ServiceUtils {
static nestedTraverse(
nodeTree: TreeNode<ITreeNodeObject>[],
partIndex: number,
parts: string[],
obj: ITreeNodeObject,
parent: ITreeNodeObject,
delimiter: string,
) {
if (parts.length <= partIndex) {
return;
}
const end = partIndex === parts.length - 1;
const partName = parts[partIndex];
for (let i = 0; i < nodeTree.length; i++) {
if (nodeTree[i].node.name !== parts[partIndex]) {
continue;
}
if (end && nodeTree[i].node.id !== obj.id) {
// Another node with the same name.
nodeTree.push(new TreeNode(obj, partName, parent));
return;
}
ServiceUtils.nestedTraverse(
nodeTree[i].children,
partIndex + 1,
parts,
obj,
nodeTree[i].node,
delimiter,
);
return;
}
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
if (end) {
nodeTree.push(new TreeNode(obj, partName, parent));
return;
}
const newPartName = parts[partIndex] + delimiter + parts[partIndex + 1];
ServiceUtils.nestedTraverse(
nodeTree,
0,
[newPartName, ...parts.slice(partIndex + 2)],
obj,
parent,
delimiter,
);
}
}
static getTreeNodeObject(
nodeTree: TreeNode<ITreeNodeObject>[],
id: string,
): TreeNode<ITreeNodeObject> {
for (let i = 0; i < nodeTree.length; i++) {
if (nodeTree[i].node.id === id) {
return nodeTree[i];
} else if (nodeTree[i].children != null) {
const node = ServiceUtils.getTreeNodeObject(nodeTree[i].children, id);
if (node !== null) {
return node;
}
}
}
return null;
}
}

View File

@@ -0,0 +1,69 @@
/**
* Use as a Decorator on async functions, it will limit how many times the function can be
* in-flight at a time.
*
* Calls beyond the limit will be queued, and run when one of the active calls finishes
*/
export function throttle(limit: number, throttleKey: (args: any[]) => string) {
return <T>(
target: any,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise<T>>,
) => {
const originalMethod: () => Promise<T> = descriptor.value;
const allThrottles = new Map<any, Map<string, (() => void)[]>>();
const getThrottles = (obj: any) => {
let throttles = allThrottles.get(obj);
if (throttles != null) {
return throttles;
}
throttles = new Map<string, (() => void)[]>();
allThrottles.set(obj, throttles);
return throttles;
};
return {
value: function (...args: any[]) {
const throttles = getThrottles(this);
const argsThrottleKey = throttleKey(args);
let queue = throttles.get(argsThrottleKey);
if (queue == null) {
queue = [];
throttles.set(argsThrottleKey, queue);
}
return new Promise<T>((resolve, reject) => {
const exec = () => {
const onFinally = () => {
queue.splice(queue.indexOf(exec), 1);
if (queue.length >= limit) {
queue[limit - 1]();
} else if (queue.length === 0) {
throttles.delete(argsThrottleKey);
if (throttles.size === 0) {
allThrottles.delete(this);
}
}
};
originalMethod
.apply(this, args)
.then((val: any) => {
onFinally();
return val;
})
.catch((err: any) => {
onFinally();
throw err;
})
.then(resolve, reject);
};
queue.push(exec);
if (queue.length <= limit) {
exec();
}
});
},
};
};
}

View File

@@ -1,11 +1,9 @@
/* eslint-disable no-useless-escape */
import * as url from "url";
import { I18nService } from "../abstractions/i18n.service";
import * as tldjs from "tldjs";
const nodeURL = typeof window === "undefined" ? url : null;
const nodeURL = typeof window === "undefined" ? require("url") : null;
export class Utils {
static inited = false;
@@ -36,7 +34,7 @@ export class Utils {
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
}
static fromB64ToArray(str: string): Uint8Array<ArrayBuffer> {
static fromB64ToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "base64"));
} else {
@@ -49,11 +47,11 @@ export class Utils {
}
}
static fromUrlB64ToArray(str: string): Uint8Array<ArrayBuffer> {
static fromUrlB64ToArray(str: string): Uint8Array {
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
}
static fromHexToArray(str: string): Uint8Array<ArrayBuffer> {
static fromHexToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "hex"));
} else {
@@ -65,7 +63,7 @@ export class Utils {
}
}
static fromUtf8ToArray(str: string): Uint8Array<ArrayBuffer> {
static fromUtf8ToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "utf8"));
} else {
@@ -78,7 +76,7 @@ export class Utils {
}
}
static fromByteStringToArray(str: string): Uint8Array<ArrayBuffer> {
static fromByteStringToArray(str: string): Uint8Array {
const arr = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
arr[i] = str.charCodeAt(i);
@@ -99,8 +97,8 @@ export class Utils {
}
}
static fromBufferToUrlB64(buffer: Uint8Array<ArrayBuffer>): string {
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer.buffer));
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
}
static fromB64toUrlB64(b64Str: string) {
@@ -249,7 +247,7 @@ export class Utils {
const urlDomain =
tldjs != null && tldjs.getDomain != null ? tldjs.getDomain(url.hostname) : null;
return urlDomain != null ? urlDomain : url.hostname;
} catch {
} catch (e) {
// Invalid domain, try another approach below.
}
}
@@ -397,7 +395,7 @@ export class Utils {
anchor.href = uriString;
return anchor as any;
}
} catch {
} catch (e) {
// Ignore error
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
import { BaseResponse } from "../response/baseResponse";
export class PermissionsApi extends BaseResponse {
accessEventLogs: boolean;
accessImportExport: boolean;
accessReports: boolean;
/**
* @deprecated Sep 29 2021: This permission has been split out to `createNewCollections`, `editAnyCollection`, and
* `deleteAnyCollection`. It exists here for backwards compatibility with Server versions <= 1.43.0
*/
manageAllCollections: boolean;
createNewCollections: boolean;
editAnyCollection: boolean;
deleteAnyCollection: boolean;
/**
* @deprecated Sep 29 2021: This permission has been split out to `editAssignedCollections` and
* `deleteAssignedCollections`. It exists here for backwards compatibility with Server versions <= 1.43.0
*/
manageAssignedCollections: boolean;
editAssignedCollections: boolean;
deleteAssignedCollections: boolean;
manageCiphers: boolean;
manageGroups: boolean;
manageSso: boolean;
managePolicies: boolean;
manageUsers: boolean;
manageResetPassword: boolean;
constructor(data: any = null) {
super(data);
if (data == null) {
return this;
}
this.accessEventLogs = this.getResponseProperty("AccessEventLogs");
this.accessImportExport = this.getResponseProperty("AccessImportExport");
this.accessReports = this.getResponseProperty("AccessReports");
// For backwards compatibility with Server <= 1.43.0
this.manageAllCollections = this.getResponseProperty("ManageAllCollections");
this.manageAssignedCollections = this.getResponseProperty("ManageAssignedCollections");
this.createNewCollections = this.getResponseProperty("CreateNewCollections");
this.editAnyCollection = this.getResponseProperty("EditAnyCollection");
this.deleteAnyCollection = this.getResponseProperty("DeleteAnyCollection");
this.editAssignedCollections = this.getResponseProperty("EditAssignedCollections");
this.deleteAssignedCollections = this.getResponseProperty("DeleteAssignedCollections");
this.manageCiphers = this.getResponseProperty("ManageCiphers");
this.manageGroups = this.getResponseProperty("ManageGroups");
this.manageSso = this.getResponseProperty("ManageSso");
this.managePolicies = this.getResponseProperty("ManagePolicies");
this.manageUsers = this.getResponseProperty("ManageUsers");
this.manageResetPassword = this.getResponseProperty("ManageResetPassword");
}
}

View File

@@ -0,0 +1,136 @@
import {
OpenIdConnectRedirectBehavior,
Saml2BindingType,
Saml2NameIdFormat,
Saml2SigningBehavior,
SsoType,
} from "../../enums/ssoEnums";
import { BaseResponse } from "../response/baseResponse";
import { SsoConfigView } from "../view/ssoConfigView";
export class SsoConfigApi extends BaseResponse {
static fromView(view: SsoConfigView, api = new SsoConfigApi()) {
api.configType = view.configType;
api.keyConnectorEnabled = view.keyConnectorEnabled;
api.keyConnectorUrl = view.keyConnectorUrl;
if (api.configType === SsoType.OpenIdConnect) {
api.authority = view.openId.authority;
api.clientId = view.openId.clientId;
api.clientSecret = view.openId.clientSecret;
api.metadataAddress = view.openId.metadataAddress;
api.redirectBehavior = view.openId.redirectBehavior;
api.getClaimsFromUserInfoEndpoint = view.openId.getClaimsFromUserInfoEndpoint;
api.additionalScopes = view.openId.additionalScopes;
api.additionalUserIdClaimTypes = view.openId.additionalUserIdClaimTypes;
api.additionalEmailClaimTypes = view.openId.additionalEmailClaimTypes;
api.additionalNameClaimTypes = view.openId.additionalNameClaimTypes;
api.acrValues = view.openId.acrValues;
api.expectedReturnAcrValue = view.openId.expectedReturnAcrValue;
} else if (api.configType === SsoType.Saml2) {
api.spNameIdFormat = view.saml.spNameIdFormat;
api.spOutboundSigningAlgorithm = view.saml.spOutboundSigningAlgorithm;
api.spSigningBehavior = view.saml.spSigningBehavior;
api.spMinIncomingSigningAlgorithm = view.saml.spMinIncomingSigningAlgorithm;
api.spWantAssertionsSigned = view.saml.spWantAssertionsSigned;
api.spValidateCertificates = view.saml.spValidateCertificates;
api.idpEntityId = view.saml.idpEntityId;
api.idpBindingType = view.saml.idpBindingType;
api.idpSingleSignOnServiceUrl = view.saml.idpSingleSignOnServiceUrl;
api.idpSingleLogoutServiceUrl = view.saml.idpSingleLogoutServiceUrl;
api.idpX509PublicCert = view.saml.idpX509PublicCert;
api.idpOutboundSigningAlgorithm = view.saml.idpOutboundSigningAlgorithm;
api.idpAllowUnsolicitedAuthnResponse = view.saml.idpAllowUnsolicitedAuthnResponse;
api.idpWantAuthnRequestsSigned = view.saml.idpWantAuthnRequestsSigned;
// Value is inverted in the api model (disable instead of allow)
api.idpDisableOutboundLogoutRequests = !view.saml.idpAllowOutboundLogoutRequests;
}
return api;
}
configType: SsoType;
keyConnectorEnabled: boolean;
keyConnectorUrl: string;
// OpenId
authority: string;
clientId: string;
clientSecret: string;
metadataAddress: string;
redirectBehavior: OpenIdConnectRedirectBehavior;
getClaimsFromUserInfoEndpoint: boolean;
additionalScopes: string;
additionalUserIdClaimTypes: string;
additionalEmailClaimTypes: string;
additionalNameClaimTypes: string;
acrValues: string;
expectedReturnAcrValue: string;
// SAML
spNameIdFormat: Saml2NameIdFormat;
spOutboundSigningAlgorithm: string;
spSigningBehavior: Saml2SigningBehavior;
spMinIncomingSigningAlgorithm: boolean;
spWantAssertionsSigned: boolean;
spValidateCertificates: boolean;
idpEntityId: string;
idpBindingType: Saml2BindingType;
idpSingleSignOnServiceUrl: string;
idpSingleLogoutServiceUrl: string;
idpX509PublicCert: string;
idpOutboundSigningAlgorithm: string;
idpAllowUnsolicitedAuthnResponse: boolean;
idpDisableOutboundLogoutRequests: boolean;
idpWantAuthnRequestsSigned: boolean;
constructor(data: any = null) {
super(data);
if (data == null) {
return;
}
this.configType = this.getResponseProperty("ConfigType");
this.keyConnectorEnabled = this.getResponseProperty("KeyConnectorEnabled");
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
this.authority = this.getResponseProperty("Authority");
this.clientId = this.getResponseProperty("ClientId");
this.clientSecret = this.getResponseProperty("ClientSecret");
this.metadataAddress = this.getResponseProperty("MetadataAddress");
this.redirectBehavior = this.getResponseProperty("RedirectBehavior");
this.getClaimsFromUserInfoEndpoint = this.getResponseProperty("GetClaimsFromUserInfoEndpoint");
this.additionalScopes = this.getResponseProperty("AdditionalScopes");
this.additionalUserIdClaimTypes = this.getResponseProperty("AdditionalUserIdClaimTypes");
this.additionalEmailClaimTypes = this.getResponseProperty("AdditionalEmailClaimTypes");
this.additionalNameClaimTypes = this.getResponseProperty("AdditionalNameClaimTypes");
this.acrValues = this.getResponseProperty("AcrValues");
this.expectedReturnAcrValue = this.getResponseProperty("ExpectedReturnAcrValue");
this.spNameIdFormat = this.getResponseProperty("SpNameIdFormat");
this.spOutboundSigningAlgorithm = this.getResponseProperty("SpOutboundSigningAlgorithm");
this.spSigningBehavior = this.getResponseProperty("SpSigningBehavior");
this.spMinIncomingSigningAlgorithm = this.getResponseProperty("SpMinIncomingSigningAlgorithm");
this.spWantAssertionsSigned = this.getResponseProperty("SpWantAssertionsSigned");
this.spValidateCertificates = this.getResponseProperty("SpValidateCertificates");
this.idpEntityId = this.getResponseProperty("IdpEntityId");
this.idpBindingType = this.getResponseProperty("IdpBindingType");
this.idpSingleSignOnServiceUrl = this.getResponseProperty("IdpSingleSignOnServiceUrl");
this.idpSingleLogoutServiceUrl = this.getResponseProperty("IdpSingleLogoutServiceUrl");
this.idpX509PublicCert = this.getResponseProperty("IdpX509PublicCert");
this.idpOutboundSigningAlgorithm = this.getResponseProperty("IdpOutboundSigningAlgorithm");
this.idpAllowUnsolicitedAuthnResponse = this.getResponseProperty(
"IdpAllowUnsolicitedAuthnResponse",
);
this.idpDisableOutboundLogoutRequests = this.getResponseProperty(
"IdpDisableOutboundLogoutRequests",
);
this.idpWantAuthnRequestsSigned = this.getResponseProperty("IdpWantAuthnRequestsSigned");
}
}

View File

@@ -0,0 +1,78 @@
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
import { OrganizationUserType } from "../../enums/organizationUserType";
import { ProductType } from "../../enums/productType";
import { PermissionsApi } from "../api/permissionsApi";
import { ProfileOrganizationResponse } from "../response/profileOrganizationResponse";
export class OrganizationData {
id: string;
name: string;
status: OrganizationUserStatusType;
type: OrganizationUserType;
enabled: boolean;
usePolicies: boolean;
useGroups: boolean;
useDirectory: boolean;
useEvents: boolean;
useTotp: boolean;
use2fa: boolean;
useApi: boolean;
useSso: boolean;
useKeyConnector: boolean;
useResetPassword: boolean;
selfHost: boolean;
usersGetPremium: boolean;
seats: number;
maxCollections: number;
maxStorageGb?: number;
ssoBound: boolean;
identifier: string;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
userId: string;
hasPublicAndPrivateKeys: boolean;
providerId: string;
providerName: string;
isProviderUser: boolean;
familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean;
planProductType: ProductType;
keyConnectorEnabled: boolean;
keyConnectorUrl: string;
constructor(response: ProfileOrganizationResponse) {
this.id = response.id;
this.name = response.name;
this.status = response.status;
this.type = response.type;
this.enabled = response.enabled;
this.usePolicies = response.usePolicies;
this.useGroups = response.useGroups;
this.useDirectory = response.useDirectory;
this.useEvents = response.useEvents;
this.useTotp = response.useTotp;
this.use2fa = response.use2fa;
this.useApi = response.useApi;
this.useSso = response.useSso;
this.useKeyConnector = response.useKeyConnector;
this.useResetPassword = response.useResetPassword;
this.selfHost = response.selfHost;
this.usersGetPremium = response.usersGetPremium;
this.seats = response.seats;
this.maxCollections = response.maxCollections;
this.maxStorageGb = response.maxStorageGb;
this.ssoBound = response.ssoBound;
this.identifier = response.identifier;
this.permissions = response.permissions;
this.resetPasswordEnrolled = response.resetPasswordEnrolled;
this.userId = response.userId;
this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys;
this.providerId = response.providerId;
this.providerName = response.providerName;
this.familySponsorshipFriendlyName = response.familySponsorshipFriendlyName;
this.familySponsorshipAvailable = response.familySponsorshipAvailable;
this.planProductType = response.planProductType;
this.keyConnectorEnabled = response.keyConnectorEnabled;
this.keyConnectorUrl = response.keyConnectorUrl;
}
}

View File

@@ -0,0 +1,23 @@
import { ProviderUserStatusType } from "../../enums/providerUserStatusType";
import { ProviderUserType } from "../../enums/providerUserType";
import { ProfileProviderResponse } from "../response/profileProviderResponse";
export class ProviderData {
id: string;
name: string;
status: ProviderUserStatusType;
type: ProviderUserType;
enabled: boolean;
userId: string;
useEvents: boolean;
constructor(response: ProfileProviderResponse) {
this.id = response.id;
this.name = response.name;
this.status = response.status;
this.type = response.type;
this.enabled = response.enabled;
this.userId = response.userId;
this.useEvents = response.useEvents;
}
}

View File

@@ -0,0 +1,151 @@
import { AuthenticationStatus } from "../../enums/authenticationStatus";
import { KdfType } from "../../enums/kdfType";
import { UriMatchType } from "../../enums/uriMatchType";
import { OrganizationData } from "../data/organizationData";
import { ProviderData } from "../data/providerData";
import { EncString } from "./encString";
import { EnvironmentUrls } from "./environmentUrls";
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
export class EncryptionPair<TEncrypted, TDecrypted> {
encrypted?: TEncrypted;
decrypted?: TDecrypted;
}
export class DataEncryptionPair<TEncrypted, TDecrypted> {
encrypted?: { [id: string]: TEncrypted };
decrypted?: TDecrypted[];
}
export class AccountData {
ciphers?: any = new DataEncryptionPair<any, any>();
folders?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
localData?: any;
sends?: any = new DataEncryptionPair<any, any>();
collections?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
policies?: DataEncryptionPair<any, any> = new DataEncryptionPair<any, any>();
passwordGenerationHistory?: EncryptionPair<any[], any[]> = new EncryptionPair<any[], any[]>();
addEditCipherInfo?: any;
eventCollection?: any[];
organizations?: { [id: string]: OrganizationData };
providers?: { [id: string]: ProviderData };
}
export class AccountKeys {
cryptoMasterKey?: SymmetricCryptoKey;
cryptoMasterKeyAuto?: string;
cryptoMasterKeyB64?: string;
cryptoMasterKeyBiometric?: string;
cryptoSymmetricKey?: EncryptionPair<string, SymmetricCryptoKey> = new EncryptionPair<
string,
SymmetricCryptoKey
>();
organizationKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
any,
Map<string, SymmetricCryptoKey>
>();
providerKeys?: EncryptionPair<any, Map<string, SymmetricCryptoKey>> = new EncryptionPair<
any,
Map<string, SymmetricCryptoKey>
>();
privateKey?: EncryptionPair<string, ArrayBuffer> = new EncryptionPair<string, ArrayBuffer>();
legacyEtmKey?: SymmetricCryptoKey;
publicKey?: ArrayBuffer;
apiKeyClientSecret?: string;
}
export class AccountProfile {
apiKeyClientId?: string;
authenticationStatus?: AuthenticationStatus;
convertAccountToKeyConnector?: boolean;
email?: string;
emailVerified?: boolean;
entityId?: string;
entityType?: string;
everBeenUnlocked?: boolean;
forcePasswordReset?: boolean;
hasPremiumPersonally?: boolean;
lastSync?: string;
userId?: string;
usesKeyConnector?: boolean;
keyHash?: string;
kdfIterations?: number;
kdfType?: KdfType;
}
export class AccountSettings {
autoConfirmFingerPrints?: boolean;
autoFillOnPageLoadDefault?: boolean;
biometricLocked?: boolean;
biometricUnlock?: boolean;
clearClipboard?: number;
collapsedGroupings?: string[];
defaultUriMatch?: UriMatchType;
disableAddLoginNotification?: boolean;
disableAutoBiometricsPrompt?: boolean;
disableAutoTotpCopy?: boolean;
disableBadgeCounter?: boolean;
disableChangedPasswordNotification?: boolean;
disableContextMenuItem?: boolean;
disableGa?: boolean;
dontShowCardsCurrentTab?: boolean;
dontShowIdentitiesCurrentTab?: boolean;
enableAlwaysOnTop?: boolean;
enableAutoFillOnPageLoad?: boolean;
enableBiometric?: boolean;
enableFullWidth?: boolean;
enableGravitars?: boolean;
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
equivalentDomains?: any;
minimizeOnCopyToClipboard?: boolean;
neverDomains?: { [id: string]: any };
passwordGenerationOptions?: any;
usernameGenerationOptions?: any;
generatorOptions?: any;
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
protectedPin?: string;
settings?: any; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number;
vaultTimeoutAction?: string = "lock";
}
export class AccountTokens {
accessToken?: string;
decodedToken?: any;
refreshToken?: string;
securityStamp?: string;
}
export class Account {
data?: AccountData = new AccountData();
keys?: AccountKeys = new AccountKeys();
profile?: AccountProfile = new AccountProfile();
settings?: AccountSettings = new AccountSettings();
tokens?: AccountTokens = new AccountTokens();
constructor(init: Partial<Account>) {
Object.assign(this, {
data: {
...new AccountData(),
...init?.data,
},
keys: {
...new AccountKeys(),
...init?.keys,
},
profile: {
...new AccountProfile(),
...init?.profile,
},
settings: {
...new AccountSettings(),
...init?.settings,
},
tokens: {
...new AccountTokens(),
...init?.tokens,
},
});
}
}

View File

@@ -0,0 +1,82 @@
import { View } from "../view/view";
import { EncString } from "./encString";
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
export default class Domain {
protected buildDomainModel<D extends Domain>(
domain: D,
dataObj: any,
map: any,
notEncList: any[] = [],
) {
for (const prop in map) {
// eslint-disable-next-line
if (!map.hasOwnProperty(prop)) {
continue;
}
const objProp = dataObj[map[prop] || prop];
if (notEncList.indexOf(prop) > -1) {
(domain as any)[prop] = objProp ? objProp : null;
} else {
(domain as any)[prop] = objProp ? new EncString(objProp) : null;
}
}
}
protected buildDataModel<D extends Domain>(
domain: D,
dataObj: any,
map: any,
notEncStringList: any[] = [],
) {
for (const prop in map) {
// eslint-disable-next-line
if (!map.hasOwnProperty(prop)) {
continue;
}
const objProp = (domain as any)[map[prop] || prop];
if (notEncStringList.indexOf(prop) > -1) {
(dataObj as any)[prop] = objProp != null ? objProp : null;
} else {
(dataObj as any)[prop] = objProp != null ? (objProp as EncString).encryptedString : null;
}
}
}
protected async decryptObj<T extends View>(
viewModel: T,
map: any,
orgId: string,
key: SymmetricCryptoKey = null,
): Promise<T> {
const promises = [];
const self: any = this;
for (const prop in map) {
// eslint-disable-next-line
if (!map.hasOwnProperty(prop)) {
continue;
}
(function (theProp) {
const p = Promise.resolve()
.then(() => {
const mapProp = map[theProp] || theProp;
if (self[mapProp]) {
return self[mapProp].decrypt(orgId, key);
}
return null;
})
.then((val: any) => {
(viewModel as any)[theProp] = val;
});
promises.push(p);
})(prop);
}
await Promise.all(promises);
return viewModel;
}
}

View File

@@ -0,0 +1,3 @@
export class EncArrayBuffer {
constructor(public buffer: ArrayBuffer) {}
}

View File

@@ -0,0 +1,122 @@
import { CryptoService } from "../../abstractions/crypto.service";
import { EncryptionType } from "../../enums/encryptionType";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
export class EncString {
encryptedString?: string;
encryptionType?: EncryptionType;
decryptedValue?: string;
data?: string;
iv?: string;
mac?: string;
constructor(
encryptedStringOrType: string | EncryptionType,
data?: string,
iv?: string,
mac?: string,
) {
if (data != null) {
// data and header
const encType = encryptedStringOrType as EncryptionType;
if (iv != null) {
this.encryptedString = encType + "." + iv + "|" + data;
} else {
this.encryptedString = encType + "." + data;
}
// mac
if (mac != null) {
this.encryptedString += "|" + mac;
}
this.encryptionType = encType;
this.data = data;
this.iv = iv;
this.mac = mac;
return;
}
this.encryptedString = encryptedStringOrType as string;
if (!this.encryptedString) {
return;
}
const headerPieces = this.encryptedString.split(".");
let encPieces: string[] = null;
if (headerPieces.length === 2) {
try {
this.encryptionType = parseInt(headerPieces[0], null);
encPieces = headerPieces[1].split("|");
} catch (e) {
return;
}
} else {
encPieces = this.encryptedString.split("|");
this.encryptionType =
encPieces.length === 3
? EncryptionType.AesCbc128_HmacSha256_B64
: EncryptionType.AesCbc256_B64;
}
switch (this.encryptionType) {
case EncryptionType.AesCbc128_HmacSha256_B64:
case EncryptionType.AesCbc256_HmacSha256_B64:
if (encPieces.length !== 3) {
return;
}
this.iv = encPieces[0];
this.data = encPieces[1];
this.mac = encPieces[2];
break;
case EncryptionType.AesCbc256_B64:
if (encPieces.length !== 2) {
return;
}
this.iv = encPieces[0];
this.data = encPieces[1];
break;
case EncryptionType.Rsa2048_OaepSha256_B64:
case EncryptionType.Rsa2048_OaepSha1_B64:
if (encPieces.length !== 1) {
return;
}
this.data = encPieces[0];
break;
default:
return;
}
}
async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise<string> {
if (this.decryptedValue != null) {
return this.decryptedValue;
}
let cryptoService: CryptoService;
const containerService = (Utils.global as any).bitwardenContainerService;
if (containerService) {
cryptoService = containerService.getCryptoService();
} else {
throw new Error("global bitwardenContainerService not initialized.");
}
try {
if (key == null) {
key = await cryptoService.getOrgKey(orgId);
}
this.decryptedValue = await cryptoService.decryptToUtf8(this, key);
} catch (e) {
this.decryptedValue = "[error: cannot decrypt]";
}
return this.decryptedValue;
}
}

View File

@@ -0,0 +1,8 @@
import { SymmetricCryptoKey } from "./symmetricCryptoKey";
export class EncryptedObject {
iv: ArrayBuffer;
data: ArrayBuffer;
mac: ArrayBuffer;
key: SymmetricCryptoKey;
}

View File

@@ -2,5 +2,9 @@ export class EnvironmentUrls {
base: string = null;
api: string = null;
identity: string = null;
icons: string = null;
notifications: string = null;
events: string = null;
webVault: string = null;
keyConnector: string = null;
}

View File

@@ -0,0 +1,40 @@
import { StateVersion } from "../../enums/stateVersion";
import { ThemeType } from "../../enums/themeType";
import { EnvironmentUrls } from "./environmentUrls";
import { WindowState } from "./windowState";
export class GlobalState {
enableAlwaysOnTop?: boolean;
installedVersion?: string;
locale?: string = "en";
organizationInvitation?: any;
ssoCodeVerifier?: string;
ssoOrganizationIdentifier?: string;
ssoState?: string;
rememberedEmail?: string;
theme?: ThemeType = ThemeType.System;
window?: WindowState = new WindowState();
twoFactorToken?: string;
disableFavicon?: boolean;
biometricAwaitingAcceptance?: boolean;
biometricFingerprintValidated?: boolean;
vaultTimeout?: number;
vaultTimeoutAction?: string;
loginRedirect?: any;
mainWindowSize?: number;
enableBiometrics?: boolean;
biometricText?: string;
noAutoPromptBiometrics?: boolean;
noAutoPromptBiometricsText?: string;
stateVersion: StateVersion = StateVersion.One;
environmentUrls: EnvironmentUrls = new EnvironmentUrls();
enableTray?: boolean;
enableMinimizeToTray?: boolean;
enableCloseToTray?: boolean;
enableStartToTray?: boolean;
openAtLogin?: boolean;
alwaysShowDock?: boolean;
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
}

View File

@@ -0,0 +1,184 @@
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
import { OrganizationUserType } from "../../enums/organizationUserType";
import { ProductType } from "../../enums/productType";
import { PermissionsApi } from "../api/permissionsApi";
import { OrganizationData } from "../data/organizationData";
export class Organization {
id: string;
name: string;
status: OrganizationUserStatusType;
type: OrganizationUserType;
enabled: boolean;
usePolicies: boolean;
useGroups: boolean;
useDirectory: boolean;
useEvents: boolean;
useTotp: boolean;
use2fa: boolean;
useApi: boolean;
useSso: boolean;
useKeyConnector: boolean;
useResetPassword: boolean;
selfHost: boolean;
usersGetPremium: boolean;
seats: number;
maxCollections: number;
maxStorageGb?: number;
ssoBound: boolean;
identifier: string;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
userId: string;
hasPublicAndPrivateKeys: boolean;
providerId: string;
providerName: string;
isProviderUser: boolean;
familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean;
planProductType: ProductType;
keyConnectorEnabled: boolean;
keyConnectorUrl: string;
constructor(obj?: OrganizationData) {
if (obj == null) {
return;
}
this.id = obj.id;
this.name = obj.name;
this.status = obj.status;
this.type = obj.type;
this.enabled = obj.enabled;
this.usePolicies = obj.usePolicies;
this.useGroups = obj.useGroups;
this.useDirectory = obj.useDirectory;
this.useEvents = obj.useEvents;
this.useTotp = obj.useTotp;
this.use2fa = obj.use2fa;
this.useApi = obj.useApi;
this.useSso = obj.useSso;
this.useKeyConnector = obj.useKeyConnector;
this.useResetPassword = obj.useResetPassword;
this.selfHost = obj.selfHost;
this.usersGetPremium = obj.usersGetPremium;
this.seats = obj.seats;
this.maxCollections = obj.maxCollections;
this.maxStorageGb = obj.maxStorageGb;
this.ssoBound = obj.ssoBound;
this.identifier = obj.identifier;
this.permissions = obj.permissions;
this.resetPasswordEnrolled = obj.resetPasswordEnrolled;
this.userId = obj.userId;
this.hasPublicAndPrivateKeys = obj.hasPublicAndPrivateKeys;
this.providerId = obj.providerId;
this.providerName = obj.providerName;
this.isProviderUser = obj.isProviderUser;
this.familySponsorshipFriendlyName = obj.familySponsorshipFriendlyName;
this.familySponsorshipAvailable = obj.familySponsorshipAvailable;
this.planProductType = obj.planProductType;
this.keyConnectorEnabled = obj.keyConnectorEnabled;
this.keyConnectorUrl = obj.keyConnectorUrl;
}
get canAccess() {
if (this.type === OrganizationUserType.Owner) {
return true;
}
return this.enabled && this.status === OrganizationUserStatusType.Confirmed;
}
get isManager() {
return (
this.type === OrganizationUserType.Manager ||
this.type === OrganizationUserType.Owner ||
this.type === OrganizationUserType.Admin
);
}
get isAdmin() {
return this.type === OrganizationUserType.Owner || this.type === OrganizationUserType.Admin;
}
get isOwner() {
return this.type === OrganizationUserType.Owner || this.isProviderUser;
}
get canAccessEventLogs() {
return this.isAdmin || this.permissions.accessEventLogs;
}
get canAccessImportExport() {
return this.isAdmin || this.permissions.accessImportExport;
}
get canAccessReports() {
return this.isAdmin || this.permissions.accessReports;
}
get canCreateNewCollections() {
return (
this.isManager ||
(this.permissions.createNewCollections ?? this.permissions.manageAllCollections)
);
}
get canEditAnyCollection() {
return (
this.isAdmin || (this.permissions.editAnyCollection ?? this.permissions.manageAllCollections)
);
}
get canDeleteAnyCollection() {
return (
this.isAdmin ||
(this.permissions.deleteAnyCollection ?? this.permissions.manageAllCollections)
);
}
get canViewAllCollections() {
return this.canCreateNewCollections || this.canEditAnyCollection || this.canDeleteAnyCollection;
}
get canEditAssignedCollections() {
return (
this.isManager ||
(this.permissions.editAssignedCollections ?? this.permissions.manageAssignedCollections)
);
}
get canDeleteAssignedCollections() {
return (
this.isManager ||
(this.permissions.deleteAssignedCollections ?? this.permissions.manageAssignedCollections)
);
}
get canViewAssignedCollections() {
return this.canDeleteAssignedCollections || this.canEditAssignedCollections;
}
get canManageGroups() {
return this.isAdmin || this.permissions.manageGroups;
}
get canManageSso() {
return this.isAdmin || this.permissions.manageSso;
}
get canManagePolicies() {
return this.isAdmin || this.permissions.managePolicies;
}
get canManageUsers() {
return this.isAdmin || this.permissions.manageUsers;
}
get canManageUsersPassword() {
return this.isAdmin || this.permissions.manageResetPassword;
}
get isExemptFromPolicies() {
return this.canManagePolicies;
}
}

View File

@@ -0,0 +1,50 @@
import { ProviderUserStatusType } from "../../enums/providerUserStatusType";
import { ProviderUserType } from "../../enums/providerUserType";
import { ProviderData } from "../data/providerData";
export class Provider {
id: string;
name: string;
status: ProviderUserStatusType;
type: ProviderUserType;
enabled: boolean;
userId: string;
useEvents: boolean;
constructor(obj?: ProviderData) {
if (obj == null) {
return;
}
this.id = obj.id;
this.name = obj.name;
this.status = obj.status;
this.type = obj.type;
this.enabled = obj.enabled;
this.userId = obj.userId;
this.useEvents = obj.useEvents;
}
get canAccess() {
if (this.isProviderAdmin) {
return true;
}
return this.enabled && this.status === ProviderUserStatusType.Confirmed;
}
get canCreateOrganizations() {
return this.enabled && this.isProviderAdmin;
}
get canManageUsers() {
return this.isProviderAdmin;
}
get canAccessEventLogs() {
return this.isProviderAdmin;
}
get isProviderAdmin() {
return this.type === ProviderUserType.ProviderAdmin;
}
}

View File

@@ -0,0 +1,17 @@
import { Account } from "./account";
import { GlobalState } from "./globalState";
export class State<
TGlobalState extends GlobalState = GlobalState,
TAccount extends Account = Account,
> {
accounts: { [userId: string]: TAccount } = {};
globals: TGlobalState;
activeUserId: string;
authenticatedAccounts: string[] = [];
accountActivity: { [userId: string]: number } = {};
constructor(globals: TGlobalState) {
this.globals = globals;
}
}

View File

@@ -0,0 +1,16 @@
export class TreeNode<T extends ITreeNodeObject> {
parent: T;
node: T;
children: TreeNode<T>[] = [];
constructor(node: T, name: string, parent: T) {
this.parent = parent;
this.node = node;
this.node.name = name;
}
}
export interface ITreeNodeObject {
id: string;
name: string;
}

View File

@@ -1,4 +1,5 @@
import { ClientType } from "../../../enums/clientType";
import { Utils } from "../../../misc/utils";
import { CaptchaProtectedRequest } from "../captchaProtectedRequest";
import { DeviceRequest } from "../deviceRequest";
@@ -29,4 +30,5 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
return obj;
}
}

View File

@@ -12,6 +12,7 @@ export abstract class TokenRequest {
this.device = device != null ? device : null;
}
// eslint-disable-next-line
alterIdentityTokenHeaders(headers: Headers) {
// Implemented in subclass if required
}

View File

@@ -0,0 +1,10 @@
import { BaseResponse } from "./baseResponse";
export class ApiKeyResponse extends BaseResponse {
apiKey: string;
constructor(response: any) {
super(response);
this.apiKey = this.getResponseProperty("ApiKey");
}
}

View File

@@ -0,0 +1,83 @@
import { PaymentMethodType } from "../../enums/paymentMethodType";
import { TransactionType } from "../../enums/transactionType";
import { BaseResponse } from "./baseResponse";
export class BillingResponse extends BaseResponse {
balance: number;
paymentSource: BillingSourceResponse;
invoices: BillingInvoiceResponse[] = [];
transactions: BillingTransactionResponse[] = [];
constructor(response: any) {
super(response);
this.balance = this.getResponseProperty("Balance");
const paymentSource = this.getResponseProperty("PaymentSource");
const transactions = this.getResponseProperty("Transactions");
const invoices = this.getResponseProperty("Invoices");
this.paymentSource = paymentSource == null ? null : new BillingSourceResponse(paymentSource);
if (transactions != null) {
this.transactions = transactions.map((t: any) => new BillingTransactionResponse(t));
}
if (invoices != null) {
this.invoices = invoices.map((i: any) => new BillingInvoiceResponse(i));
}
}
}
export class BillingSourceResponse extends BaseResponse {
type: PaymentMethodType;
cardBrand: string;
description: string;
needsVerification: boolean;
constructor(response: any) {
super(response);
this.type = this.getResponseProperty("Type");
this.cardBrand = this.getResponseProperty("CardBrand");
this.description = this.getResponseProperty("Description");
this.needsVerification = this.getResponseProperty("NeedsVerification");
}
}
export class BillingInvoiceResponse extends BaseResponse {
url: string;
pdfUrl: string;
number: string;
paid: boolean;
date: string;
amount: number;
constructor(response: any) {
super(response);
this.url = this.getResponseProperty("Url");
this.pdfUrl = this.getResponseProperty("PdfUrl");
this.number = this.getResponseProperty("Number");
this.paid = this.getResponseProperty("Paid");
this.date = this.getResponseProperty("Date");
this.amount = this.getResponseProperty("Amount");
}
}
export class BillingTransactionResponse extends BaseResponse {
createdDate: string;
amount: number;
refunded: boolean;
partiallyRefunded: boolean;
refundedAmount: number;
type: TransactionType;
paymentMethodType: PaymentMethodType;
details: string;
constructor(response: any) {
super(response);
this.createdDate = this.getResponseProperty("CreatedDate");
this.amount = this.getResponseProperty("Amount");
this.refunded = this.getResponseProperty("Refunded");
this.partiallyRefunded = this.getResponseProperty("PartiallyRefunded");
this.refundedAmount = this.getResponseProperty("RefundedAmount");
this.type = this.getResponseProperty("Type");
this.paymentMethodType = this.getResponseProperty("PaymentMethodType");
this.details = this.getResponseProperty("Details");
}
}

View File

@@ -0,0 +1,32 @@
import { BaseResponse } from "./baseResponse";
export class BreachAccountResponse extends BaseResponse {
addedDate: string;
breachDate: string;
dataClasses: string[];
description: string;
domain: string;
isActive: boolean;
isVerified: boolean;
logoPath: string;
modifiedDate: string;
name: string;
pwnCount: number;
title: string;
constructor(response: any) {
super(response);
this.addedDate = this.getResponseProperty("AddedDate");
this.breachDate = this.getResponseProperty("BreachDate");
this.dataClasses = this.getResponseProperty("DataClasses");
this.description = this.getResponseProperty("Description");
this.domain = this.getResponseProperty("Domain");
this.isActive = this.getResponseProperty("IsActive");
this.isVerified = this.getResponseProperty("IsVerified");
this.logoPath = this.getResponseProperty("LogoPath");
this.modifiedDate = this.getResponseProperty("ModifiedDate");
this.name = this.getResponseProperty("Name");
this.pwnCount = this.getResponseProperty("PwnCount");
this.title = this.getResponseProperty("Title");
}
}

View File

@@ -0,0 +1,20 @@
import { DeviceType } from "../../enums/deviceType";
import { BaseResponse } from "./baseResponse";
export class DeviceResponse extends BaseResponse {
id: string;
name: number;
identifier: string;
type: DeviceType;
creationDate: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.name = this.getResponseProperty("Name");
this.identifier = this.getResponseProperty("Identifier");
this.type = this.getResponseProperty("Type");
this.creationDate = this.getResponseProperty("CreationDate");
}
}

View File

@@ -0,0 +1,20 @@
import { BaseResponse } from "./baseResponse";
import { GlobalDomainResponse } from "./globalDomainResponse";
export class DomainsResponse extends BaseResponse {
equivalentDomains: string[][];
globalEquivalentDomains: GlobalDomainResponse[] = [];
constructor(response: any) {
super(response);
this.equivalentDomains = this.getResponseProperty("EquivalentDomains");
const globalEquivalentDomains = this.getResponseProperty("GlobalEquivalentDomains");
if (globalEquivalentDomains != null) {
this.globalEquivalentDomains = globalEquivalentDomains.map(
(d: any) => new GlobalDomainResponse(d),
);
} else {
this.globalEquivalentDomains = [];
}
}
}

View File

@@ -0,0 +1,14 @@
import { BaseResponse } from "./baseResponse";
export class GlobalDomainResponse extends BaseResponse {
type: number;
domains: string[];
excluded: boolean;
constructor(response: any) {
super(response);
this.type = this.getResponseProperty("Type");
this.domains = this.getResponseProperty("Domains");
this.excluded = this.getResponseProperty("Excluded");
}
}

View File

@@ -0,0 +1,31 @@
import { BaseResponse } from "./baseResponse";
import { SelectionReadOnlyResponse } from "./selectionReadOnlyResponse";
export class GroupResponse extends BaseResponse {
id: string;
organizationId: string;
name: string;
accessAll: boolean;
externalId: string;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.organizationId = this.getResponseProperty("OrganizationId");
this.name = this.getResponseProperty("Name");
this.accessAll = this.getResponseProperty("AccessAll");
this.externalId = this.getResponseProperty("ExternalId");
}
}
export class GroupDetailsResponse extends GroupResponse {
collections: SelectionReadOnlyResponse[] = [];
constructor(response: any) {
super(response);
const collections = this.getResponseProperty("Collections");
if (collections != null) {
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
}
}
}

Some files were not shown because too many files have changed in this diff Show More