1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-02-26 09:23:12 +00:00

Compare commits

...

52 Commits

Author SHA1 Message Date
Brandon
a74973ccfa remove unused jslib code 2026-02-25 16:44:38 -05:00
Brandon
4eb74cdeb4 cleanup migration key for account, clean up 2026-02-25 15:41:15 -05:00
Brandon
b1a3859516 remove hard coded state keys, fix env urls, fix account state migration 2026-02-25 10:01:35 -05:00
Brandon
4c7afc0e64 Merge branch 'state-service-rewrite' into dev-clarity-hands-on 2026-02-24 10:04:35 -05:00
Brandon
4b079a3ec9 continue removing jslib code 2026-02-23 15:50:31 -05:00
Brandon
77873c3075 migrate electron, window/tray, and UI state to vNext 2026-02-23 14:07:46 -05:00
Brandon
9997e988e6 update callers to vNext state service 2026-02-23 10:32:05 -05:00
Brandon
abdddacb06 merge state service re-write 2026-02-20 16:20:03 -05:00
Brandon
d5566c56b1 Migrate all remaining cjs files to esm 2026-02-19 17:45:04 -05:00
Brandon
a019555143 migrate configuration files to ESM 2026-02-19 17:01:08 -05:00
Brandon
b3cb369ed8 add skill for migrating CJS to ESM + example 2026-02-17 10:36:24 -05:00
Brandon
06edf4cf91 flatten account structure using claude 2026-02-13 16:34:20 -05:00
Brandon
623382f9e1 add tech debt context for DC Modernization 2026-02-11 11:02:56 -05:00
Mick Letofsky
1aad9e1cbe Slim down and align with our current practices (#994) 2026-02-11 15:58:42 +01:00
Brandon Treston
3059934d4c remove substitute (#992) 2026-02-10 09:41:26 -05:00
Vincent Salucci
42cf13df08 chore: bump version to 2026.2.0 (#993) 2026-02-09 14:11:35 -06:00
renovate[bot]
1a9f0a2ca7 [deps]: Update babel-loader to v10 (#987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 15:08:41 +00:00
renovate[bot]
30b3595de3 [deps]: Update typescript-eslint monorepo to v8.54.0 (#976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 14:34:42 +00:00
Brandon
a0e74948bd fix integration test 2026-02-05 12:04:41 -05:00
Brandon
9f8018e8f8 fix type issues 2026-02-05 11:53:24 -05:00
renovate[bot]
28f0ff4b24 [deps]: Update angular-cli monorepo to v21.1.2 (#982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 10:11:28 -06:00
Brandon
0bff38c459 add tests 2026-02-04 16:00:01 -05:00
renovate[bot]
14fc69c810 [deps]: Update ngx-toastr to v20 (#989)
* [deps]: Update ngx-toastr to v20

* Adjust to toastr v20

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Sven <svernyi@bitwarden.com>
2026-02-04 13:44:40 -06:00
renovate[bot]
1ad0aea61f [deps]: Update prettier to v3.8.1 (#985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 13:27:17 -05:00
renovate[bot]
f41156969c [deps]: Update angular monorepo (#981)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jared <TheWolfBadger@gmail.com>
2026-02-04 13:20:42 -05:00
renovate[bot]
39b151b1e0 [deps]: Update mini-css-extract-plugin to v2.10.0 (#984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 13:05:34 -05:00
renovate[bot]
483f26fa6f [deps]: Update type-fest to v5.4.2 (#986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 11:39:06 -05:00
renovate[bot]
8849385d1b [deps]: Update @angular/cdk to v21.1.1 (#980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 09:41:55 -05:00
renovate[bot]
a7aff97360 [deps]: Lock file maintenance (#978)
* [deps]: Lock file maintenance

* add COEP and COOP headers to enabled SharedArrayBuffer

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon <btreston@bitwarden.com>
2026-02-02 11:49:42 -05:00
Brandon
94ff20f69f scaffold new state service, add migration, initial commit 2026-01-29 14:36:38 -05:00
renovate[bot]
7381857296 [deps]: Update gh minor (#973)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-29 14:19:11 -05:00
renovate[bot]
ba17d5b438 [deps]: Update electron-updater to v6.7.3 (#974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-26 11:06:25 -06:00
Daniel James Smith
b5d31e693b Replace deprecated codecov/test-results-action with codecov/codecov-action with report_type set to test_results (#979)
https://github.com/codecov/test-results-action?tab=readme-ov-file#%EF%B8%8F-deprecation-warning-%EF%B8%8F

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
2026-01-23 08:06:18 -06:00
Brandon Treston
2854a2eba1 Update Angular to v21 (#972)
* update jest to v.30.2.0

* ng update 21 wip

* update @angular/cdk@21

* NG 21 WIP

* @ngtools-webpack@21 and jest-preset-angular@16

* updated jest, add babel & jest-enveironment-jsdom

* add missing polyfils for TextEncoder & TextDecoder

* cleanup lock file

* tsconfig cleanup

* fix import

* cleanup

* clean up
2026-01-21 08:37:52 +10:00
renovate[bot]
4485ecab3c [deps]: Update ldapts to v8.1.3 (#975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-20 11:44:49 +10:00
renovate[bot]
9e3b2d2d95 [deps]: Update jest-mock-extended to v4 (#977)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-19 09:42:05 -05:00
renovate[bot]
b2997358dc [deps]: Lock file maintenance (#834)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-14 08:07:48 +10:00
renovate[bot]
db258f0191 [deps]: Update @angular/compiler to v20.3.16 [SECURITY] (#967)
* [deps]: Update @angular/compiler to v20.3.16 [SECURITY]

* Upgrade all Angular packages

* Downgrade jest-mock-extended to support Jest 29

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2026-01-14 07:36:46 +10:00
Vincent Salucci
19d7884933 chore: bump version to 2026.1.0 (#969) 2026-01-12 11:32:43 -06:00
Jared McCannon
21ce02f431 [PM-26889] - Typescript 5.9 upgrade with updates (#965)
* [deps]: Update typescript to v5.9.3

* Updated return types.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-09 10:07:27 -06:00
renovate[bot]
1af8fc1067 [deps]: Update gh minor (#955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 15:30:59 +10:00
renovate[bot]
6c2f54bad5 [deps]: Update webpack to v5.104.1 (#963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-08 14:51:53 +10:00
renovate[bot]
bb9a6a61ee [deps]: Update sass to v1.97.1 (#956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 15:00:07 -05:00
renovate[bot]
f0a19b6267 [deps]: Update actions/upload-artifact action to v6 (#958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-06 14:51:01 +00:00
Mick Letofsky
220d6c02c7 Revert review Code Triggered by labeled event (#962) 2025-12-31 11:04:31 -05:00
Mick Letofsky
321db6e771 Review Code Triggered by labeled event (#961) 2025-12-30 18:15:46 +01:00
Daniel James Smith
554e14d7a8 Update copyright year to 2026 (#960) 2025-12-30 07:50:18 +10:00
renovate[bot]
f195e27938 [deps]: Update typescript-eslint monorepo to v8.50.0 (#957)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 10:33:04 +00:00
renovate[bot]
d1ac1e667e [deps]: Update eslint to v9 (#867)
* [deps]: Update eslint to v9

* resolve lint errors, upgrade eslint, replace unmaintained packages

* refresh lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Brandon <btreston@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2025-12-19 19:48:36 -06:00
Mick Letofsky
b9867b131f Remove additional code review prompt file (#954) 2025-12-19 16:57:48 +01:00
renovate[bot]
bb165441ee [deps]: Update @types/node to v22.19.2 (#910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-17 14:58:52 -06:00
renovate[bot]
b8964aa382 [deps]: Update angular-eslint monorepo to v20.7.0 (#940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 11:47:23 -06:00
197 changed files with 9357 additions and 26454 deletions

View File

@@ -1,3 +1,7 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Bitwarden Directory Connector
## Project Overview
@@ -20,6 +24,392 @@ Directory Connector is a TypeScript application that synchronizes users and grou
- 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
@@ -45,6 +435,32 @@ jslib/ # Legacy folder structure (mix of deprecated/unused and c
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

239
.claude/plan.md Normal file
View File

@@ -0,0 +1,239 @@
# 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,27 +0,0 @@
Please review this pull request with a focus on:
- Code quality and best practices
- Potential bugs or issues
- Security implications
- Performance considerations
Note: The PR branch is already checked out in the current working directory.
Provide a comprehensive review including:
- Summary of changes since last review
- Critical issues found (be thorough)
- Suggested improvements (be thorough)
- Good practices observed (be concise - list only the most notable items without elaboration)
- Action items for the author
- Leverage collapsible <details> sections where appropriate for lengthy explanations or code
snippets to enhance human readability
When reviewing subsequent commits:
- Track status of previously identified issues (fixed/unfixed/reopened)
- Identify NEW problems introduced since last review
- Note if fixes introduced new issues
IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note
what was done well without explaining why or praising excessively.

View File

@@ -0,0 +1,130 @@
---
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)

View File

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

View File

@@ -1,95 +0,0 @@
{
"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"
}
}
]
}

View File

@@ -9,26 +9,3 @@
## 📸 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

@@ -23,7 +23,7 @@ jobs:
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -51,12 +51,12 @@ jobs:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
@@ -129,12 +129,12 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -209,7 +209,7 @@ jobs:
choco install checksum --no-progress
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest.yml
path: ./dist/latest.yml
@@ -379,12 +379,12 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -439,12 +439,12 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml

View File

@@ -40,7 +40,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -52,7 +52,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -129,7 +129,7 @@ jobs:
- name: Report test results
id: report
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
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()
@@ -140,7 +140,9 @@ jobs:
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
- name: Upload results to codecov.io
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results

View File

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

View File

@@ -2,7 +2,7 @@ name: Code Review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
types: [opened, synchronize, reopened]
permissions: {}

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -34,7 +34,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -53,7 +53,7 @@ jobs:
run: npm run test --coverage
- name: Report test results
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
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()
@@ -64,7 +64,9 @@ jobs:
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@5a605bd92782ce0810fa3b8acc235c921b497052 # v5.2.0
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
- name: Upload results to codecov.io
uses: codecov/test-results-action@4e79e65778be1cecd5df25e14af1eafb6df80ea9 # v1.0.2
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results

View File

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

View File

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

View File

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

149
eslint.config.mjs Normal file
View File

@@ -0,0 +1,149 @@
// @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 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
import { pathsToModuleNameMapper } from "ts-jest";
import tsconfig from "./tsconfig.json" with { type: "json" };
const tsPreset = require("ts-jest/jest-preset");
const angularPreset = require("jest-preset-angular/jest-preset");
const { defaultTransformerOptions } = require("jest-preset-angular/presets");
import angularPresetsModule from "jest-preset-angular/presets/index.js";
const { defaultTransformerOptions } = angularPresetsModule;
const { compilerOptions } = tsconfig;
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
// ...tsPreset,
// ...angularPreset,
export default {
preset: "jest-preset-angular",
reporters: ["default", "jest-junit"],
@@ -26,7 +26,6 @@ module.exports = {
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

745
jslib-removal-plan.md Normal file
View File

@@ -0,0 +1,745 @@
# 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

@@ -1,28 +0,0 @@
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

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

View File

@@ -1,75 +1,77 @@
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 as BaseToast,
ToastPackage,
ToastrService,
TOAST_CONFIG,
} from "ngx-toastr";
import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } from "ngx-toastr";
@Component({
selector: "[toast-component2]",
template: `
<button
*ngIf="options.closeButton"
(click)="remove()"
type="button"
class="toast-close-button"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
@if (options().closeButton) {
<button (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>
<div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title">
{{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container>
</div>
<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>
@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>
}
`,
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;
}
}
`,
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 BaseToast {
constructor(
protected toastrService: ToastrService,
public toastPackage: ToastPackage,
) {
super(toastrService, toastPackage);
}
}
export class BitwardenToast extends Toast {}
export const BitwardenToastGlobalConfig: GlobalConfig = {
...DefaultNoComponentGlobalConfig,

View File

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

View File

@@ -1,143 +0,0 @@
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 } from "rxjs/operators";
import { first, firstValueFrom } from "rxjs";
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 modalRef.onCreated.pipe(first()).toPromise();
await firstValueFrom(modalRef.onCreated);
return [modalRef, modalComponentRef.instance.componentRef.instance];
}

View File

@@ -1,195 +0,0 @@
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

@@ -1,69 +0,0 @@
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

@@ -1,127 +0,0 @@
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

@@ -1,110 +0,0 @@
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

@@ -1,102 +0,0 @@
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

@@ -1,84 +0,0 @@
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

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

View File

@@ -1,7 +1,3 @@
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;
@@ -21,17 +17,10 @@ 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;
return arr.buffer;
}

View File

@@ -1,86 +0,0 @@
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,34 +1,2 @@
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;
}
// Stub file - re-exports DC EnvironmentService
export { EnvironmentService, EnvironmentUrls } from "@/src/abstractions/environment.service";

View File

@@ -1,218 +1,2 @@
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>;
}
// Stub file - re-exports DC StateService
export { StateService } from "@/src/abstractions/state.service";

View File

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

View File

@@ -1,32 +1,2 @@
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>;
}
// Stub file - re-exports DC TokenService
export { TokenService } from "@/src/abstractions/token.service";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
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

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

View File

@@ -1,14 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
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,5 +3,6 @@ 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
Latest = Four,
Five = 5, // New state service implementation
Latest = Five,
}

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
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

@@ -1,13 +0,0 @@
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

@@ -1,29 +0,0 @@
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,9 +26,4 @@ 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

@@ -1,72 +0,0 @@
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

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
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

@@ -1,136 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,151 +0,0 @@
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

@@ -1,82 +0,0 @@
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

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

View File

@@ -1,122 +0,0 @@
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

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

View File

@@ -2,9 +2,5 @@ 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

@@ -1,40 +0,0 @@
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

@@ -1,184 +0,0 @@
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

@@ -1,50 +0,0 @@
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

@@ -1,17 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -1,83 +0,0 @@
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

@@ -1,32 +0,0 @@
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

@@ -1,20 +0,0 @@
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

@@ -1,20 +0,0 @@
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

@@ -1,14 +0,0 @@
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

@@ -1,31 +0,0 @@
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));
}
}
}

View File

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

View File

@@ -1,12 +0,0 @@
import { BaseResponse } from "./baseResponse";
export class KeysResponse extends BaseResponse {
privateKey: string;
publicKey: string;
constructor(response: any) {
super(response);
this.privateKey = this.getResponseProperty("PrivateKey");
this.publicKey = this.getResponseProperty("PublicKey");
}
}

View File

@@ -1,13 +0,0 @@
import { BaseResponse } from "./baseResponse";
export class ListResponse<T> extends BaseResponse {
data: T[];
continuationToken: string;
constructor(response: any, t: new (dataResponse: any) => T) {
super(response);
const data = this.getResponseProperty("Data");
this.data = data == null ? [] : data.map((dr: any) => new t(dr));
this.continuationToken = this.getResponseProperty("ContinuationToken");
}
}

View File

@@ -1,98 +0,0 @@
import { NotificationType } from "../../enums/notificationType";
import { BaseResponse } from "./baseResponse";
export class NotificationResponse extends BaseResponse {
contextId: string;
type: NotificationType;
payload: any;
constructor(response: any) {
super(response);
this.contextId = this.getResponseProperty("ContextId");
this.type = this.getResponseProperty("Type");
const payload = this.getResponseProperty("Payload");
switch (this.type) {
case NotificationType.SyncCipherCreate:
case NotificationType.SyncCipherDelete:
case NotificationType.SyncCipherUpdate:
case NotificationType.SyncLoginDelete:
this.payload = new SyncCipherNotification(payload);
break;
case NotificationType.SyncFolderCreate:
case NotificationType.SyncFolderDelete:
case NotificationType.SyncFolderUpdate:
this.payload = new SyncFolderNotification(payload);
break;
case NotificationType.SyncVault:
case NotificationType.SyncCiphers:
case NotificationType.SyncOrgKeys:
case NotificationType.SyncSettings:
case NotificationType.LogOut:
this.payload = new UserNotification(payload);
break;
case NotificationType.SyncSendCreate:
case NotificationType.SyncSendUpdate:
case NotificationType.SyncSendDelete:
this.payload = new SyncSendNotification(payload);
break;
default:
break;
}
}
}
export class SyncCipherNotification extends BaseResponse {
id: string;
userId: string;
organizationId: string;
collectionIds: string[];
revisionDate: Date;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.userId = this.getResponseProperty("UserId");
this.organizationId = this.getResponseProperty("OrganizationId");
this.collectionIds = this.getResponseProperty("CollectionIds");
this.revisionDate = new Date(this.getResponseProperty("RevisionDate"));
}
}
export class SyncFolderNotification extends BaseResponse {
id: string;
userId: string;
revisionDate: Date;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.userId = this.getResponseProperty("UserId");
this.revisionDate = new Date(this.getResponseProperty("RevisionDate"));
}
}
export class UserNotification extends BaseResponse {
userId: string;
date: Date;
constructor(response: any) {
super(response);
this.userId = this.getResponseProperty("UserId");
this.date = new Date(this.getResponseProperty("Date"));
}
}
export class SyncSendNotification extends BaseResponse {
id: string;
userId: string;
revisionDate: Date;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.userId = this.getResponseProperty("UserId");
this.revisionDate = new Date(this.getResponseProperty("RevisionDate"));
}
}

View File

@@ -1,35 +0,0 @@
import { SsoConfigApi } from "../../api/ssoConfigApi";
import { BaseResponse } from "../baseResponse";
export class OrganizationSsoResponse extends BaseResponse {
enabled: boolean;
data: SsoConfigApi;
urls: SsoUrls;
constructor(response: any) {
super(response);
this.enabled = this.getResponseProperty("Enabled");
this.data =
this.getResponseProperty("Data") != null
? new SsoConfigApi(this.getResponseProperty("Data"))
: null;
this.urls = new SsoUrls(this.getResponseProperty("Urls"));
}
}
class SsoUrls extends BaseResponse {
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
constructor(response: any) {
super(response);
this.callbackPath = this.getResponseProperty("CallbackPath");
this.signedOutCallbackPath = this.getResponseProperty("SignedOutCallbackPath");
this.spEntityId = this.getResponseProperty("SpEntityId");
this.spMetadataUrl = this.getResponseProperty("SpMetadataUrl");
this.spAcsUrl = this.getResponseProperty("SpAcsUrl");
}
}

View File

@@ -1,12 +0,0 @@
import { BaseResponse } from "./baseResponse";
export class OrganizationAutoEnrollStatusResponse extends BaseResponse {
id: string;
resetPasswordEnabled: boolean;
constructor(response: any) {
super(response);
this.id = this.getResponseProperty("Id");
this.resetPasswordEnabled = this.getResponseProperty("ResetPasswordEnabled");
}
}

View File

@@ -1,7 +0,0 @@
import { KeysResponse } from "./keysResponse";
export class OrganizationKeysResponse extends KeysResponse {
constructor(response: any) {
super(response);
}
}

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