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

Compare commits

..

4 Commits

Author SHA1 Message Date
Vince Grassia
def3caa93b Add support for latest.yml artifacts (#168)
(cherry picked from commit 36cc6552bf)
2021-10-27 16:28:45 -04:00
Vince Grassia
0b881a63d4 Rename artifact before upload (#167)
(cherry picked from commit 05b5fd2eb4)
2021-10-27 12:38:15 -04:00
Vince Grassia
9dadfbd037 Bump version to 2.9.7 (#166)
(cherry picked from commit 95f1e86509)
2021-10-27 11:05:29 -04:00
Joseph Flinn
b0bf07d70e Updating the release constraints (#163)
* Updating the release constraints

* removing the master branch release ci code execution

* updating some verbiage

(cherry picked from commit 314adeb164)
2021-10-22 09:16:35 -07:00
396 changed files with 27531 additions and 420669 deletions

View File

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

View File

@@ -1 +0,0 @@
ignores: ["*-loader", "webpack-cli", "@types/jest"]

View File

@@ -7,9 +7,10 @@ root = true
[*]
end_of_line = lf
insert_final_newline = true
quote_type = single
# Set default charset
[*.{js,ts,scss,html}]
charset = utf-8
indent_style = space
indent_size = 2
indent_size = 4

View File

@@ -1,2 +0,0 @@
# Apply Prettier https://github.com/bitwarden/directory-connector/pull/194
096196fcd512944d1c3d9c007647a1319b032639

19
.github/CODEOWNERS vendored
View File

@@ -1,19 +0,0 @@
# Please sort into logical groups with comment headers. Sort groups in order of specificity.
# For example, default owners should always be the first group.
# Sort lines alphabetically within these groups to avoid accidentally adding duplicates.
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default file owners.
* @bitwarden/team-admin-console-dev
# Docker-related files
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
# Claude related files
.claude/ @bitwarden/team-ai-sme
.github/workflows/respond.yml @bitwarden/team-ai-sme
.github/workflows/review-code.yml @bitwarden/team-ai-sme

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
## 🎟️ Tracking
<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->
## 📔 Objective
<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->
## 📸 Screenshots
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->

View File

@@ -1,24 +0,0 @@
{
$schema: "https://docs.renovatebot.com/renovate-schema.json",
extends: ["github>bitwarden/renovate-config"],
enabledManagers: ["github-actions", "npm"],
packageRules: [
{
groupName: "gh minor",
matchManagers: ["github-actions"],
matchUpdateTypes: ["minor", "patch"],
},
],
ignoreDeps: [
// yao-pkg is used to create a single executable application bundle for the CLI.
// It is a third party build of node which carries a high supply chain risk.
// This must be manually vetted by our appsec team before upgrading.
// It is excluded from renovate to avoid accidentally upgrading to a non-vetted version.
"@yao-pkg/pkg",
// googleapis uses ESM after 149.0.0 so we are not upgrading it until we have ESM support.
// They release new versions every couple of weeks so ignoring it at the dependency dashboard
// level is not sufficient.
// FIXME: remove and upgrade when we have ESM support.
"googleapis",
],
}

BIN
.github/secrets/devid-app-cert.p12.gpg vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
.github/secrets/macdev-cert.p12.gpg vendored Normal file

Binary file not shown.

View File

@@ -1,399 +1,339 @@
---
name: Build
on:
pull_request: {}
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
workflow_dispatch: {}
branches-ignore:
- 'l10n_master'
permissions:
contents: read
jobs:
setup:
name: Setup
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
cloc:
name: CLOC
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Set up CLOC
run: |
sudo apt update
sudo apt -y install cloc
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
setup:
name: Setup
runs-on: ubuntu-20.04
outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Get Package Version
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version package.json)
echo "package_version=$PKG_VERSION" >> "$GITHUB_OUTPUT"
PKG_VERSION=$(jq -r .version src/package.json)
echo "::set-output name=package_version::$PKG_VERSION"
- name: Get Node Version
id: retrieve-node-version
run: |
NODE_NVMRC=$(cat .nvmrc)
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
linux-cli:
name: Build Linux CLI
runs-on: ubuntu-24.04
cli:
name: CLI
runs-on: windows-2019
needs: setup
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
permissions:
contents: read
_WIN_PKG_FETCH_VERSION: 14.17.6
_WIN_PKG_VERSION: 3.2
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Keytar
run: |
keytarVersion=$(cat package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-linux-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/linux
wget "$keytarUrl" -O "./keytar/linux/$keytarTarGz"
tar -xvf "./keytar/linux/$keytarTarGz" -C ./keytar/linux
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli:lin
- name: Zip
run: zip -j "dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip" "dist-cli/linux/bwdc" "keytar/linux/build/Release/keytar.node"
- name: Version Test
run: |
sudo apt-get update
sudo apt install libsecret-1-0 dbus-x11 gnome-keyring
eval "$(dbus-launch --sh-syntax)"
eval "$(echo -n "" | /usr/bin/gnome-keyring-daemon --login)"
eval "$(/usr/bin/gnome-keyring-daemon --components=secrets --start)"
mkdir -p test/linux
unzip "./dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip" -d ./test/linux
testVersion=$(./test/linux/bwdc -v)
echo "version: $_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
echo "Version test failed."
exit 1
fi
- name: Upload Linux Zip to GitHub
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
if-no-files-found: error
macos-cli:
name: Build Mac CLI
runs-on: macos-15-intel
needs: setup
permissions:
contents: read
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Keytar
run: |
keytarVersion=$(cat package.json | jq -r '.dependencies.keytar')
keytarTar="keytar-v$keytarVersion-napi-v3-darwin-x64.tar"
keytarTarGz="$keytarTar.gz"
keytarUrl="https://github.com/atom/node-keytar/releases/download/v$keytarVersion/$keytarTarGz"
mkdir -p ./keytar/macos
wget "$keytarUrl" -O "./keytar/macos/$keytarTarGz"
tar -xvf "./keytar/macos/$keytarTarGz" -C ./keytar/macos
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli:mac
- name: Zip
run: zip -j "dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip" "dist-cli/macos/bwdc" "keytar/macos/build/Release/keytar.node"
- name: Version Test
run: |
mkdir -p test/macos
unzip "./dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip" -d ./test/macos
testVersion=$(./test/macos/bwdc -v)
echo "version: $_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if [ "$testVersion" != "$_PACKAGE_VERSION" ]; then
echo "Version test failed."
exit 1
fi
- name: Upload Mac Zip to GitHub
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
if-no-files-found: error
windows-cli:
name: Build Windows CLI
runs-on: windows-2022
needs: setup
permissions:
contents: read
env:
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup Windows builder
run: |
choco install checksum --no-progress
choco install reshack --no-progress
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- name: Setting WIN_PKG
run: |
echo "WIN_PKG=$env:WIN_PKG" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "version: $env:pkgVersion"
env:
WIN_PKG: C:\Users\runneradmin\.pkg-cache\v3.0\fetched-v14.16.1-win-x64
- name: Get pkg-fetch
shell: pwsh
run: |
cd $HOME
$fetchedUrl = "https://github.com/vercel/pkg-fetch/releases/download/v$env:_WIN_PKG_VERSION/node-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
New-Item -ItemType directory -Path ./.pkg-cache
New-Item -ItemType directory -Path ./.pkg-cache/v$env:_WIN_PKG_VERSION
Invoke-RestMethod -Uri $fetchedUrl `
-OutFile "./.pkg-cache/v$env:_WIN_PKG_VERSION/fetched-v$env:_WIN_PKG_FETCH_VERSION-win-x64"
- name: Keytar
shell: pwsh
run: |
$keytarVersion = (Get-Content -Raw -Path ./package.json | ConvertFrom-Json).dependencies.keytar
$keytarTar = "keytar-v${keytarVersion}-napi-v3-{0}-x64.tar"
$keytarVersion = (Get-Content -Raw -Path ./src/package.json | ConvertFrom-Json).dependencies.keytar
$nodeModVersion = node -e "console.log(process.config.variables.node_module_version)"
$keytarTar = "keytar-v${keytarVersion}-node-v${nodeModVersion}-{0}-x64.tar"
$keytarTarGz = "${keytarTar}.gz"
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
New-Item -ItemType directory -Path ./keytar/macos | Out-Null
New-Item -ItemType directory -Path ./keytar/linux | Out-Null
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
Invoke-RestMethod -Uri $($keytarUrl -f "darwin") -OutFile "./keytar/macos/$($keytarTarGz -f "darwin")"
Invoke-RestMethod -Uri $($keytarUrl -f "linux") -OutFile "./keytar/linux/$($keytarTarGz -f "linux")"
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
7z e "./keytar/macos/$($keytarTarGz -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTarGz -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
7z e "./keytar/macos/$($keytarTar -f "darwin")" -o"./keytar/macos"
7z e "./keytar/linux/$($keytarTar -f "linux")" -o"./keytar/linux"
7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
- name: Setup Version Info
shell: pwsh
run: |
$major, $minor, $patch = $env:_PACKAGE_VERSION.split('.')
$versionInfo = @"
1 VERSIONINFO
FILEVERSION $major,$minor,$patch,0
PRODUCTVERSION $major,$minor,$patch,0
FILEOS 0x40004
FILETYPE 0x1
{
BLOCK "StringFileInfo"
{
BLOCK "040904b0"
{
VALUE "CompanyName", "Bitwarden Inc."
VALUE "ProductName", "Bitwarden"
VALUE "FileDescription", "Bitwarden Directory Connector CLI"
VALUE "FileVersion", "$env:_PACKAGE_VERSION"
VALUE "ProductVersion", "$env:_PACKAGE_VERSION"
VALUE "OriginalFilename", "bwdc.exe"
VALUE "InternalName", "bwdc"
VALUE "LegalCopyright", "Copyright Bitwarden Inc."
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0409 0x04B0
}
}
"@
$versionInfo | Out-File ./version-info.rc
- name: Resource Hacker
shell: cmd
run: |
set PATH=%PATH%;C:\Program Files (x86)\Resource Hacker
set WIN_PKG=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\fetched-v%_WIN_PKG_FETCH_VERSION%-win-x64
set WIN_PKG_BUILT=C:\Users\runneradmin\.pkg-cache\v%_WIN_PKG_VERSION%\built-v%_WIN_PKG_FETCH_VERSION%-win-x64
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action delete -mask ICONGROUP,1,
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG% -save %WIN_PKG% -action addoverwrite -resource version-info.res
- name: Install
run: npm install
- name: Package CLI
run: npm run dist:cli:win
run: npm run dist:cli
- name: Zip
shell: cmd
run: 7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
run: |
7z a ./dist-cli/bwdc-windows-%_PACKAGE_VERSION%.zip ./dist-cli/windows/bwdc.exe ./keytar/windows/keytar.node
7z a ./dist-cli/bwdc-macos-%_PACKAGE_VERSION%.zip ./dist-cli/macos/bwdc ./keytar/macos/keytar.node
7z a ./dist-cli/bwdc-linux-%_PACKAGE_VERSION%.zip ./dist-cli/linux/bwdc ./keytar/linux/keytar.node
- name: Version Test
shell: pwsh
run: |
Expand-Archive -Path "dist-cli\bwdc-windows-$env:_PACKAGE_VERSION.zip" -DestinationPath "test\windows"
$testVersion = Invoke-Expression '& .\test\windows\bwdc.exe -v'
echo "version: ${env:_PACKAGE_VERSION}"
Expand-Archive -Path "./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/windows"
$testVersion = Invoke-Expression '& ./test/windows/bwdc.exe -v'
echo "version: $env:_PACKAGE_VERSION"
echo "testVersion: $testVersion"
if ($testVersion -ne ${env:_PACKAGE_VERSION}) {
if($testVersion -ne $env:_PACKAGE_VERSION) {
Throw "Version test failed."
}
- name: Create checksums
run: |
checksum -f="./dist-cli/bwdc-windows-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-windows-sha256-${env:_PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-macos-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-macos-sha256-${env:_PACKAGE_VERSION}.txt
checksum -f="./dist-cli/bwdc-linux-${env:_PACKAGE_VERSION}.zip" `
-t sha256 | Out-File ./dist-cli/bwdc-linux-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload Windows checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload Mac checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-macos-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
- name: Upload Linux checksum to GitHub
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
path: ./dist-cli/bwdc-linux-sha256-${{ env._PACKAGE_VERSION }}.txt
if-no-files-found: error
windows-gui:
name: Build Windows GUI
runs-on: windows-2022
name: Windows GUI
runs-on: windows-2019
needs: setup
permissions:
contents: read
id-token: write
env:
NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up .NET
uses: actions/setup-dotnet@a71d1eb2c86af85faa8c772c03fb365e377e45ea
with:
persist-credentials: false
dotnet-version: "3.1.x"
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install $(node -v)
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
shell: pwsh
- name: Print environment
run: |
node --version
npm --version
dotnet --version
- name: Install AST
run: dotnet tool install --global AzureSignTool --version 4.0.1
uses: bitwarden/gh-actions/install-ast@f135c42c8596cb535c5bcb7523c0b2eef89709ac
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Install Node dependencies
run: npm install
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "code-signing-vault-url,
code-signing-client-id,
code-signing-tenant-id,
code-signing-client-secret,
code-signing-cert-name"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Run linter
run: npm run lint
- name: Build & Sign
run: npm run dist:win
env:
ELECTRON_BUILDER_SIGN: 1
SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }}
SIGNING_CLIENT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-client-id }}
SIGNING_TENANT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-tenant-id }}
SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }}
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
SIGNING_VAULT_URL: ${{ secrets.SIGNING_VAULT_URL }}
SIGNING_CLIENT_ID: ${{ secrets.SIGNING_CLIENT_ID }}
SIGNING_TENANT_ID: ${{ secrets.SIGNING_TENANT_ID }}
SIGNING_CLIENT_SECRET: ${{ secrets.SIGNING_CLIENT_SECRET }}
SIGNING_CERT_NAME: ${{ secrets.SIGNING_CERT_NAME }}
- name: List Dist
run: dir ./dist
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest.yml
path: ./dist/latest.yml
if-no-files-found: error
linux-gui:
name: Build Linux GUI
runs-on: ubuntu-24.04
linux:
name: Linux
runs-on: ubuntu-20.04
needs: setup
permissions:
contents: read
env:
NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install "$(node -v)"
node-gyp install $(node -v)
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Set up environment
run: |
@@ -401,6 +341,9 @@ jobs:
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install rpm
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: NPM Install
run: npm install
@@ -411,49 +354,40 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
if-no-files-found: error
macos-gui:
name: Build MacOS GUI
runs-on: macos-15-intel
macos:
name: MacOS
runs-on: macos-11
needs: setup
permissions:
contents: read
id-token: write
env:
NODE_OPTIONS: --max_old_space_size=4096
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
npm install -g node-gyp
node-gyp install "$(node -v)"
node-gyp install $(node -v)
- name: Set Node options
run: echo "NODE_OPTIONS=--max_old_space_size=4096" >> $GITHUB_ENV
- name: Print environment
run: |
@@ -461,61 +395,53 @@ jobs:
npm --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
shell: bash
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-directory-connector
secrets: "KEYCHAIN-PASSWORD,APP-STORE-CONNECT-AUTH-KEY,APP-STORE-CONNECT-TEAM-ISSUER"
- name: Get certificates
- name: Decrypt secrets
env:
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
shell: bash
run: |
mkdir -p "$HOME/certificates"
mkdir -p $HOME/secrets
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-app-cert |
jq -r .value | base64 -d > "$HOME/certificates/devid-app-cert.p12"
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/devid-app-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/devid-app-cert.p12.gpg"
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/devid-installer-cert |
jq -r .value | base64 -d > "$HOME/certificates/devid-installer-cert.p12"
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/devid-installer-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/devid-installer-cert.p12.gpg"
az keyvault secret show --id https://bitwarden-ci.vault.azure.net/certificates/macdev-cert |
jq -r .value | base64 -d > "$HOME/certificates/macdev-cert.p12"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
gpg --quiet --batch --yes --decrypt --passphrase="$DECRYPT_FILE_PASSWORD" \
--output "$HOME/secrets/macdev-cert.p12" \
"$GITHUB_WORKSPACE/.github/secrets/macdev-cert.p12.gpg"
- name: Set up keychain
env:
KEYCHAIN_PASSWORD: ${{ steps.get-kv-secrets.outputs.KEYCHAIN-PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
DEVID_CERT_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }}
MACDEV_CERT_PASSWORD: ${{ secrets.MACDEV_CERT_PASSWORD }}
shell: bash
run: |
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security create-keychain -p $KEYCHAIN_PASSWORD build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain
security set-keychain-settings -lut 1200 build.keychain
security import "$HOME/certificates/devid-app-cert.p12" -k build.keychain -P "" \
security import "$HOME/secrets/devid-app-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import "$HOME/certificates/devid-installer-cert.p12" -k build.keychain -P "" \
security import "$HOME/secrets/devid-installer-cert.p12" -k build.keychain -P $DEVID_CERT_PASSWORD \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \
security import "$HOME/secrets/macdev-cert.p12" -k build.keychain -P $MACDEV_CERT_PASSWORD \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
- name: Load package version
run: |
$rootPath = $env:GITHUB_WORKSPACE;
$packageVersion = (Get-Content -Raw -Path "$rootPath\package.json" | ConvertFrom-Json).version;
$packageVersion = (Get-Content -Raw -Path $rootPath\src\package.json | ConvertFrom-Json).version;
Write-Output "Setting package version to $packageVersion";
Write-Output "PACKAGE_VERSION=$packageVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append;
@@ -524,46 +450,37 @@ jobs:
- name: Install Node dependencies
run: npm install
- name: Set up private auth key
env:
_APP_STORE_CONNECT_AUTH_KEY: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-AUTH-KEY }}
run: |
mkdir ~/private_keys
cat << EOF > ~/private_keys/AuthKey_UFD296548T.p8
${_APP_STORE_CONNECT_AUTH_KEY}
EOF
- name: Run linter
run: npm run lint
- name: Build application
run: npm run dist:mac
env:
APP_STORE_CONNECT_TEAM_ISSUER: ${{ steps.get-kv-secrets.outputs.APP-STORE-CONNECT-TEAM-ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: UFD296548T
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_UFD296548T.p8
CSC_FOR_PULL_REQUEST: true
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Rename Zip Artifact
run: |
cd dist
mv "Bitwarden Directory Connector-${{ env._PACKAGE_VERSION }}-mac.zip" \
"Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip"
- name: Upload .zip artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml
@@ -572,47 +489,56 @@ jobs:
check-failures:
name: Check for failures
runs-on: ubuntu-24.04
if: always()
runs-on: ubuntu-20.04
needs:
- cloc
- setup
- linux-cli
- macos-cli
- windows-cli
- cli
- windows-gui
- linux-gui
- macos-gui
permissions:
id-token: write
- linux
- macos
steps:
- name: Check if any job failed
if: |
(github.ref == 'refs/heads/main'
|| github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
SETUP_STATUS: ${{ needs.setup.result }}
CLI_STATUS: ${{ needs.cli.result }}
WINDOWS_GUI_STATUS: ${{ needs.windows-gui.result }}
LINUX_STATUS: ${{ needs.linux.result }}
MACOS_STATUS: ${{ needs.macos.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$CLI_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_GUI_STATUS" = "failure" ]; then
exit 1
elif [ "$LINUX_STATUS" = "failure" ]; then
exit 1
elif [ "$MACOS_STATUS" = "failure" ]; then
exit 1
fi
- name: Log in to Azure
- name: Login to Azure - Prod Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
if: failure()
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
if: failure()
with:
keyvault: "bitwarden-ci"
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Notify Slack on failure
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}

View File

@@ -1,18 +0,0 @@
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
permissions:
contents: read
pull-requests: read
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-24.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@a3c219da6b8fa73f6ba62b68ff09c469b3a1c024 # 2.2.2
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"

View File

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

View File

@@ -1,94 +1,72 @@
---
name: Release
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release Options'
required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
- Dry Run
permissions:
contents: read
jobs:
setup:
name: Setup
runs-on: ubuntu-24.04
permissions:
contents: read
outputs:
release_version: ${{ steps.version.outputs.version }}
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Branch check
if: ${{ inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
if [[ "$GITHUB_REF" != "refs/heads/release" ]]; then
echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "[!] Can only release from the 'release' branch"
echo "==================================="
exit 1
fi
- name: Check Release Version
id: version
uses: bitwarden/gh-actions/release-version-check@main
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
with:
release-type: ${{ inputs.release_type }}
project-type: ts
file: package.json
ref: release
- name: Retrieve Directory Connector release version
id: retrieve-version
run: |
PKG_VERSION=$(jq -r .version src/package.json)
echo "::set-output name=package_version::$PKG_VERSION"
- name: Check to make sure Mobile release version has been bumped
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
latest_ver=$(hub release -L 1 -f '%T')
latest_ver=${latest_ver:1}
echo "Latest version: $latest_ver"
ver=${{ steps.retrieve-version.outputs.package_version }}
echo "Version: $ver"
if [ "$latest_ver" = "$ver" ]; then
echo "Version has not been bumped!"
exit 1
fi
shell: bash
release:
name: Release
runs-on: ubuntu-24.04
needs: setup
permissions:
actions: read
packages: read
contents: write
steps:
- name: Download all artifacts
if: ${{ inputs.release_type != 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ github.ref_name }}
- name: Dry Run - Download all artifacts
if: ${{ inputs.release_type == 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
workflow: build.yml
workflow_conclusion: success
branch: main
branch: release
- name: Create release
if: ${{ inputs.release_type != 'Dry Run' }}
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09 # v2.8.5
env:
PKG_VERSION: ${{ needs.setup.outputs.release_version }}
PKG_VERSION: ${{ steps.retrieve-version.outputs.package_version }}
with:
artifacts: "./bwdc-windows-${{ env.PKG_VERSION }}.zip,
./bwdc-macos-${{ env.PKG_VERSION }}.zip,
./bwdc-linux-${{ env.PKG_VERSION }}.zip,
./bwdc-windows-sha256-${{ env.PKG_VERSION }}.txt,
./bwdc-macos-sha256-${{ env.PKG_VERSION }}.txt,
./bwdc-linux-sha256-${{ env.PKG_VERSION }}.txt,
./Bitwarden-Connector-Portable-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe,
./Bitwarden-Connector-Installer-${{ env.PKG_VERSION }}.exe.blockmap,
./Bitwarden-Connector-${{ env.PKG_VERSION }}-x86_64.AppImage,
./Bitwarden-Connector-${{ env.PKG_VERSION }}-mac.zip,
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg,
./Bitwarden-Connector-${{ env.PKG_VERSION }}.dmg.blockmap,
./latest-linux.yml,
./latest-mac.yml,
./latest.yml"

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
name: Scan
on:
workflow_dispatch:
push:
branches:
- "main"
pull_request:
types: [opened, synchronize, reopened]
branches-ignore:
- "main"
pull_request_target:
types: [opened, synchronize, reopened]
branches:
- "main"
permissions: {}
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
permissions:
contents: read
sast:
name: Checkmarx
uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main
needs: check-run
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
contents: read
pull-requests: write
security-events: write
id-token: write
quality:
name: Sonar
uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main
needs: check-run
secrets:
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
permissions:
contents: read
pull-requests: write
id-token: write

View File

@@ -1,72 +0,0 @@
name: Testing
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request:
permissions:
contents: read
checks: write # required by dorny/test-reporter to upload its results
jobs:
testing:
name: Run tests
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Get Node version
id: retrieve-node-version
run: |
NODE_NVMRC=$(cat .nvmrc)
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
- name: Install Node dependencies
run: npm ci
# We use isolatedModules: true which disables typechecking in tests
# Tests in apps/ are typechecked when their app is built, so we just do it here for libs/
# See https://bitwarden.atlassian.net/browse/EC-497
- name: Run typechecking
run: npm run test:types --coverage
- name: Run tests
run: npm run test --coverage
- name: Report test results
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
# PRs from the repository and all other events are OK.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
with:
name: Test Results
path: "junit.xml"
reporter: jest-junit
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
- name: Upload results to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results

View File

@@ -1,145 +0,0 @@
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
permissions: {}
jobs:
bump_version:
name: Bump Version
runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write
steps:
- name: Validate version input
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
version: ${{ inputs.version_number_override }}
- name: Log in to Azure
uses: bitwarden/gh-actions/azure-login@main
with:
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
client_id: ${{ secrets.AZURE_CLIENT_ID }}
- name: Get Azure Key Vault secrets
id: get-kv-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: gh-org-bitwarden
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
- name: Log out from Azure
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
permission-contents: write
- name: Checkout Branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true
- name: Setup git
run: |
git config user.name github-actions
git config user.email github-actions@github.com
- name: Get current version
id: current-version
run: |
CURRENT_VERSION=$(cat package.json | jq -r '.version')
echo "version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed."
exit 1
fi
# Check if version is newer.
if printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V; then
echo "Version check successful."
else
echo "Version check failed."
exit 1
fi
- name: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump Version - Package - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "./package.json"
version: ${{ inputs.version_number_override }}
- name: Bump Version - Package - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "./package.json"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set final version output
id: set-final-version-output
env:
_BUMP_VERSION_OVERRIDE_OUTCOME: ${{ steps.bump-version-override.outcome }}
_INPUT_VERSION_NUMBER_OVERRIDE: ${{ inputs.version_number_override }}
_BUMP_VERSION_AUTOMATIC_OUTCOME: ${{ steps.bump-version-automatic.outcome }}
_CALCULATE_NEXT_VERSION: ${{ steps.calculate-next-version.outputs.version }}
run: |
if [[ "$_BUMP_VERSION_OVERRIDE_OUTCOME" == "success" ]]; then
echo "version=$_INPUT_VERSION_NUMBER_OVERRIDE" >> "$GITHUB_OUTPUT"
elif [[ "$_BUMP_VERSION_AUTOMATIC_OUTCOME" == "success" ]]; then
echo "version=$_CALCULATE_NEXT_VERSION" >> "$GITHUB_OUTPUT"
fi
- name: Check if version changed
id: version-changed
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changes_to_commit=TRUE" >> "$GITHUB_OUTPUT"
else
echo "changes_to_commit=FALSE" >> "$GITHUB_OUTPUT"
echo "No changes to commit!";
fi
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
_VERSION: ${{ steps.set-final-version-output.outputs.version }}
run: git commit -m "Bumped version to $_VERSION" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git push

51
.gitignore vendored
View File

@@ -1,44 +1,17 @@
# General
.DS_Store
Thumbs.db
# Environment variables used for tests
.env
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Node
.vs
.idea
node_modules
npm-debug.log
# Build directories
dist
build
build-cli
.angular/cache
# Testing
coverage*
junit.xml*
# Misc
vwd.webinfo
dist/
dist-cli/
css/
*.crx
*.pem
*.zip
build-cli/
build/
yarn-error.log
.DS_Store
*.nupkg
*.provisionprofile
.swp
*.env

4
.gitmodules vendored Normal file
View File

@@ -0,0 +1,4 @@
[submodule "jslib"]
path = jslib
url = https://github.com/bitwarden/jslib.git
branch = master

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

1
.nvmrc
View File

@@ -1 +0,0 @@
v20

View File

@@ -1,10 +0,0 @@
# Build directories
build
build-cli
dist
# External libraries / auto synced locales
src/locales
# Github Workflows
.github/workflows

View File

@@ -1,3 +0,0 @@
{
"printWidth": 100
}

66
.vscode/launch.json vendored
View File

@@ -1,40 +1,48 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Electron: Main",
"protocol": "inspector",
"cwd": "${workspaceRoot}/build",
"runtimeArgs": ["--remote-debugging-port=9223", "."],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
{
"type": "node",
"request": "launch",
"name": "Electron: Main",
"protocol": "inspector",
"cwd": "${workspaceRoot}/build",
"runtimeArgs": [
"--remote-debugging-port=9223",
"."
],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"sourceMaps": true
},
"sourceMaps": true
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "${workspaceFolder}/build",
"sourceMaps": true
},
{
"name": "Electron: Renderer",
"type": "chrome",
"request": "attach",
"port": 9223,
"webRoot": "${workspaceFolder}/build",
"sourceMaps": true
},
{
"type": "node",
"request": "launch",
"name": "Debug CLI",
"protocol": "inspector",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/build-cli/bwdc.js",
"args": ["sync"]
"type": "node",
"request": "launch",
"name": "Debug CLI",
"protocol": "inspector",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/build-cli/bwdc.js",
"args": [
"sync"
]
}
],
"compounds": [
{
"name": "Electron: All",
"configurations": ["Electron: Main", "Electron: Renderer"]
}
{
"name": "Electron: All",
"configurations": [
"Electron: Main",
"Electron: Renderer"
]
}
]
}

View File

@@ -3,19 +3,18 @@
# Bitwarden Directory Connector
The Bitwarden Directory Connector is a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
The Bitwarden Directory Connector is a a desktop application used to sync your Bitwarden enterprise organization to an existing directory of users and groups.
Supported directories:
- Active Directory
- Any other LDAP-based directory
- Microsoft Entra ID
- Azure Active Directory
- G Suite (Google)
- Okta
The application is written using Electron with Angular and installs on Windows, macOS, and Linux distributions.
[![Platforms](https://imgur.com/SLv9paA.png "Windows, macOS, and Linux")](https://bitwarden.com/help/directory-sync/#download-and-install)
[![Platforms](https://imgur.com/SLv9paA.png "Windows, macOS, and Linux")](https://help.bitwarden.com/article/directory-sync/#download-and-install)
![Directory Connector](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/directory-connector-macos.png "Dashboard")
@@ -42,13 +41,13 @@ bwdc config --help
**Detailed Documentation**
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://bitwarden.com/help/directory-sync-cli/.
We provide detailed documentation and examples for using the Directory Connector CLI in our help center at https://help.bitwarden.com/article/directory-sync/#command-line-interface.
## Build/Run
**Requirements**
- [Node.js](https://nodejs.org) v18 (LTS)
- [Node.js](https://nodejs.org) v14
- Windows users: To compile the native node modules used in the app you will need the Visual C++ toolset, available through the standard Visual Studio installer (recommended) or by installing [`windows-build-tools`](https://github.com/felixrieseberg/windows-build-tools) through `npm`. See more at [Compiling native Addon modules](https://github.com/Microsoft/nodejs-guidelines/blob/master/windows-environment.md#compiling-native-addon-modules).
**Run the app**
@@ -74,32 +73,8 @@ You can then run commands from the `./build-cli` folder:
node ./build-cli/bwdc.js --help
```
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
### Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge 225073aa335d33ad905877b68336a9288e89ea10`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 096196fcd512944d1c3d9c007647a1319b032639`
7. Push
#### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

View File

@@ -1,11 +1,39 @@
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance!
Bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder.
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under Bitwarden's control
- Vulnerabilities in outdated versions of Bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
While researching, we'd like to ask you to refrain from:
@@ -14,8 +42,4 @@ While researching, we'd like to ask you to refrain from:
- Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers
# We want to help you!
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
Thank you for helping keep Bitwarden and our users safe!

View File

@@ -1,37 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "apps",
"cli": {
"analytics": false
},
"projects": {
"app": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": ".",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": {
"base": "dist"
},
"index": "src/index.html",
"tsConfig": "tsconfig.json",
"assets": [],
"styles": [],
"scripts": [],
"browser": "src/main.ts"
}
}
}
}
}
}

View File

@@ -1,18 +0,0 @@
services:
open-ldap:
image: bitnamilegacy/openldap:latest
hostname: openldap
environment:
- LDAP_ADMIN_USERNAME=admin
- LDAP_ADMIN_PASSWORD=admin
- LDAP_ROOT=dc=bitwarden,dc=com
- LDAP_ENABLE_TLS=yes
- LDAP_TLS_CERT_FILE=/certs/openldap.pem
- LDAP_TLS_KEY_FILE=/certs/openldap-key.pem
- LDAP_TLS_CA_FILE=/certs/rootCA.pem
volumes:
- "./utils/openldap/ldifs:/ldifs"
- "./utils/openldap/certs:/certs"
ports:
- "1389:1389"
- "1636:1636"

View File

@@ -1,67 +0,0 @@
{
"extraMetadata": {
"name": "bitwarden-directory-connector"
},
"productName": "Bitwarden Directory Connector",
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
"app": "build"
},
"afterSign": "scripts/notarize.js",
"mac": {
"artifactName": "Bitwarden-Connector-${version}-mac.${ext}",
"category": "public.app-category.productivity",
"gatekeeperAssess": false,
"hardenedRuntime": true,
"entitlements": "resources/entitlements.mac.plist",
"entitlementsInherit": "resources/entitlements.mac.plist",
"target": ["dmg", "zip"]
},
"win": {
"target": ["portable", "nsis"],
"sign": "scripts/sign.js"
},
"linux": {
"category": "Utility",
"synopsis": "Sync your user directory to your Bitwarden organization.",
"target": ["AppImage"]
},
"dmg": {
"artifactName": "Bitwarden-Connector-${version}.${ext}",
"icon": "dmg.icns",
"contents": [
{
"x": 150,
"y": 185,
"type": "file"
},
{
"x": 390,
"y": 180,
"type": "link",
"path": "/Applications"
}
],
"window": {
"width": 540,
"height": 380
}
},
"nsis": {
"oneClick": false,
"perMachine": true,
"allowToChangeInstallationDirectory": true,
"artifactName": "Bitwarden-Connector-Installer-${version}.${ext}",
"uninstallDisplayName": "${productName}",
"deleteAppDataOnUninstall": true
},
"portable": {
"artifactName": "Bitwarden-Connector-Portable-${version}.${ext}"
},
"appImage": {
"artifactName": "Bitwarden-Connector-${version}-${arch}.${ext}"
}
}

View File

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

View File

@@ -1,49 +0,0 @@
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
const tsPreset = require("ts-jest/jest-preset");
const angularPreset = require("jest-preset-angular/jest-preset");
const { defaultTransformerOptions } = require("jest-preset-angular/presets");
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
// ...tsPreset,
// ...angularPreset,
preset: "jest-preset-angular",
reporters: ["default", "jest-junit"],
collectCoverage: true,
// Ensure we collect coverage from files without tests
collectCoverageFrom: ["src/**/*.ts"],
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],
roots: ["<rootDir>"],
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
maxWorkers: 3,
transform: {
"^.+\\.tsx?$": [
"jest-preset-angular",
// 'ts-jest',
{
...defaultTransformerOptions,
tsconfig: "./tsconfig.json",
// Further workaround for memory leak, recommended here:
// https://github.com/kulshekhar/ts-jest/issues/1967#issuecomment-697494014
// Makes tests run faster and reduces size/rate of leak, but loses typechecking on test code
// See https://bitwarden.atlassian.net/browse/EC-497 for more info
isolatedModules: true,
},
],
},
};

1
jslib Submodule

Submodule jslib added at cb00604617

9
jslib/.gitignore vendored
View File

@@ -1,9 +0,0 @@
.vs
.idea
node_modules
npm-debug.log
vwd.webinfo
*.crx
*.pem
dist
coverage

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,79 +0,0 @@
import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory } from "@angular/cdk/a11y";
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ComponentRef,
ElementRef,
OnDestroy,
Type,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import { ModalService } from "../../services/modal.service";
import { ModalRef } from "./modal.ref";
@Component({
selector: "app-modal",
template: "<ng-template #modalContent></ng-template>",
})
export class DynamicModalComponent implements AfterViewInit, OnDestroy {
componentRef: ComponentRef<any>;
@ViewChild("modalContent", { read: ViewContainerRef, static: true })
modalContentRef: ViewContainerRef;
childComponentType: Type<any>;
setComponentParameters: (component: any) => void;
private focusTrap: ConfigurableFocusTrap;
constructor(
private modalService: ModalService,
private cd: ChangeDetectorRef,
private el: ElementRef<HTMLElement>,
private focusTrapFactory: ConfigurableFocusTrapFactory,
public modalRef: ModalRef,
) {}
ngAfterViewInit() {
this.loadChildComponent(this.childComponentType);
if (this.setComponentParameters != null) {
this.setComponentParameters(this.componentRef.instance);
}
this.cd.detectChanges();
this.modalRef.created(this.el.nativeElement);
this.focusTrap = this.focusTrapFactory.create(
this.el.nativeElement.querySelector(".modal-dialog"),
);
if (this.el.nativeElement.querySelector("[appAutoFocus]") == null) {
this.focusTrap.focusFirstTabbableElementWhenReady();
}
}
loadChildComponent(componentType: Type<any>) {
const componentFactory = this.modalService.resolveComponentFactory(componentType);
this.modalContentRef.clear();
this.componentRef = this.modalContentRef.createComponent(componentFactory);
}
ngOnDestroy() {
if (this.componentRef) {
this.componentRef.destroy();
}
this.focusTrap.destroy();
}
close() {
this.modalRef.close();
}
getFocus() {
const autoFocusEl = this.el.nativeElement.querySelector("[appAutoFocus]") as HTMLElement;
autoFocusEl?.focus();
}
}

View File

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

View File

@@ -1,49 +0,0 @@
import { lastValueFrom, Observable, Subject } from "rxjs";
export class ModalRef {
onCreated: Observable<HTMLElement>; // Modal added to the DOM.
onClose: Observable<any>; // Initiated close.
onClosed: Observable<any>; // Modal was closed (Remove element from DOM)
onShow: Observable<void>; // Start showing modal
onShown: Observable<void>; // Modal is fully visible
private readonly _onCreated = new Subject<HTMLElement>();
private readonly _onClose = new Subject<any>();
private readonly _onClosed = new Subject<any>();
private readonly _onShow = new Subject<void>();
private readonly _onShown = new Subject<void>();
private lastResult: any;
constructor() {
this.onCreated = this._onCreated.asObservable();
this.onClose = this._onClose.asObservable();
this.onClosed = this._onClosed.asObservable();
this.onShow = this._onShow.asObservable();
this.onShown = this._onShow.asObservable();
}
show() {
this._onShow.next();
}
shown() {
this._onShown.next();
}
close(result?: any) {
this.lastResult = result;
this._onClose.next(result);
}
closed() {
this._onClosed.next(this.lastResult);
}
created(el: HTMLElement) {
this._onCreated.next(el);
}
onClosedPromise(): Promise<any> {
return lastValueFrom(this.onClosed);
}
}

View File

@@ -1,101 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } from "ngx-toastr";
@Component({
selector: "[toast-component2]",
template: `
@if (options().closeButton) {
<button (click)="remove()" type="button" class="toast-close-button" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
}
<div class="icon">
<i></i>
</div>
<div>
@if (title()) {
<div [class]="options().titleClass" [attr.aria-label]="title()">
{{ title() }}
@if (duplicatesCount) {
[{{ duplicatesCount + 1 }}]
}
</div>
}
@if (message() && options().enableHtml) {
<div
role="alertdialog"
aria-live="polite"
[class]="options().messageClass"
[innerHTML]="message()"
></div>
}
@if (message() && !options().enableHtml) {
<div
role="alertdialog"
aria-live="polite"
[class]="options().messageClass"
[attr.aria-label]="message()"
>
{{ message() }}
</div>
}
</div>
@if (options().progressBar) {
<div>
<div class="toast-progress" [style.width]="width + '%'"></div>
</div>
}
`,
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;
}
}
`,
preserveWhitespaces: false,
standalone: false,
})
export class BitwardenToast extends Toast {}
export const BitwardenToastGlobalConfig: GlobalConfig = {
...DefaultNoComponentGlobalConfig,
toastComponent: BitwardenToast,
};
@NgModule({
imports: [CommonModule],
declarations: [BitwardenToast],
exports: [BitwardenToast],
})
export class BitwardenToastModule {
static forRoot(config: Partial<GlobalConfig> = {}): ModuleWithProviders<BitwardenToastModule> {
return {
ngModule: BitwardenToastModule,
providers: [
{
provide: TOAST_CONFIG,
useValue: {
default: BitwardenToastGlobalConfig,
config: config,
},
},
],
};
}
}

View File

@@ -1,27 +0,0 @@
import { Directive, ElementRef, Input, Renderer2 } from "@angular/core";
@Directive({
selector: "[appA11yTitle]",
standalone: false,
})
export class A11yTitleDirective {
@Input() set appA11yTitle(title: string) {
this.title = title;
}
private title: string;
constructor(
private el: ElementRef,
private renderer: Renderer2,
) {}
ngOnInit() {
if (!this.el.nativeElement.hasAttribute("title")) {
this.renderer.setAttribute(this.el.nativeElement, "title", this.title);
}
if (!this.el.nativeElement.hasAttribute("aria-label")) {
this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title);
}
}
}

View File

@@ -1,50 +0,0 @@
import { Directive, ElementRef, Input, OnChanges } from "@angular/core";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
import { ValidationService } from "../services/validation.service";
/**
* Provides error handling, in particular for any error returned by the server in an api call.
* Attach it to a <form> element and provide the name of the class property that will hold the api call promise.
* e.g. <form [appApiAction]="this.formPromise">
* Any errors/rejections that occur will be intercepted and displayed as error toasts.
*/
@Directive({
selector: "[appApiAction]",
standalone: false,
})
export class ApiActionDirective implements OnChanges {
@Input() appApiAction: Promise<any>;
constructor(
private el: ElementRef,
private validationService: ValidationService,
private logService: LogService,
) {}
ngOnChanges(changes: any) {
if (this.appApiAction == null || this.appApiAction.then == null) {
return;
}
this.el.nativeElement.loading = true;
this.appApiAction.then(
(response: any) => {
this.el.nativeElement.loading = false;
},
(e: any) => {
this.el.nativeElement.loading = false;
if ((e as ErrorResponse).captchaRequired) {
this.logService.error("Captcha required error response: " + e.getSingleMessage());
return;
}
this.logService?.error(`Received API exception: ${e}`);
this.validationService.showError(e);
},
);
}
}

View File

@@ -1,31 +0,0 @@
import { Directive, ElementRef, Input, NgZone } from "@angular/core";
import { take } from "rxjs";
import { Utils } from "@/jslib/common/src/misc/utils";
@Directive({
selector: "[appAutofocus]",
standalone: false,
})
export class AutofocusDirective {
@Input() set appAutofocus(condition: boolean | string) {
this.autofocus = condition === "" || condition === true;
}
private autofocus: boolean;
constructor(
private el: ElementRef,
private ngZone: NgZone,
) {}
ngOnInit() {
if (!Utils.isMobileBrowser && this.autofocus) {
if (this.ngZone.isStable) {
this.el.nativeElement.focus();
} else {
this.ngZone.onStable.pipe(take(1)).subscribe(() => this.el.nativeElement.focus());
}
}
}
}

View File

@@ -1,13 +0,0 @@
import { Directive, ElementRef, HostListener } from "@angular/core";
@Directive({
selector: "[appBlurClick]",
standalone: false,
})
export class BlurClickDirective {
constructor(private el: ElementRef) {}
@HostListener("click") onClick() {
this.el.nativeElement.blur();
}
}

View File

@@ -1,60 +0,0 @@
import { Directive, ElementRef, HostListener, OnInit } from "@angular/core";
@Directive({
selector: "[appBoxRow]",
standalone: false,
})
export class BoxRowDirective implements OnInit {
el: HTMLElement = null;
formEls: Element[];
constructor(elRef: ElementRef) {
this.el = elRef.nativeElement;
}
ngOnInit(): void {
this.formEls = Array.from(
this.el.querySelectorAll('input:not([type="hidden"]), select, textarea'),
);
this.formEls.forEach((formEl) => {
formEl.addEventListener(
"focus",
() => {
this.el.classList.add("active");
},
false,
);
formEl.addEventListener(
"blur",
() => {
this.el.classList.remove("active");
},
false,
);
});
}
@HostListener("click", ["$event"]) onClick(event: Event) {
const target = event.target as HTMLElement;
if (
target !== this.el &&
!target.classList.contains("progress") &&
!target.classList.contains("progress-bar")
) {
return;
}
if (this.formEls.length > 0) {
const formEl = this.formEls[0] as HTMLElement;
if (formEl.tagName.toLowerCase() === "input") {
const inputEl = formEl as HTMLInputElement;
if (inputEl.type != null && inputEl.type.toLowerCase() === "checkbox") {
inputEl.click();
return;
}
}
formEl.focus();
}
}
}

View File

@@ -1,15 +0,0 @@
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
@Directive({
selector: "[appFallbackSrc]",
standalone: false,
})
export class FallbackSrcDirective {
@Input("appFallbackSrc") appFallbackSrc: string;
constructor(private el: ElementRef) {}
@HostListener("error") onError() {
this.el.nativeElement.src = this.appFallbackSrc;
}
}

View File

@@ -1,11 +0,0 @@
import { Directive, HostListener } from "@angular/core";
@Directive({
selector: "[appStopClick]",
standalone: false,
})
export class StopClickDirective {
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
$event.preventDefault();
}
}

View File

@@ -1,11 +0,0 @@
import { Directive, HostListener } from "@angular/core";
@Directive({
selector: "[appStopProp]",
standalone: false,
})
export class StopPropDirective {
@HostListener("click", ["$event"]) onClick($event: MouseEvent) {
$event.stopPropagation();
}
}

View File

@@ -1,15 +0,0 @@
import { Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
@Pipe({
name: "i18n",
standalone: false,
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 262 KiB

View File

@@ -1,251 +0,0 @@
$icomoon-font-family: "bwi-font" !default;
$icomoon-font-path: "/jslib/angular/src/scss/bwicons/fonts/" !default;
// New font sheet? Update the font-face information below
@font-face {
font-family: "#{$icomoon-font-family}";
src:
url($icomoon-font-path + "bwi-font.svg") format("svg"),
url($icomoon-font-path + "bwi-font.ttf") format("truetype"),
url($icomoon-font-path + "bwi-font.woff") format("woff"),
url($icomoon-font-path + "bwi-font.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-display: block;
}
// Base Class
.bwi {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: "#{$icomoon-font-family}" !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
display: inline-block;
/* Better Font Rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// Fixed Width Icons
.bwi-fw {
width: calc(18em / 14);
text-align: center;
}
// Sizing Changes
.bwi-sm {
font-size: 0.875em;
}
.bwi-lg {
font-size: calc(4em / 3);
line-height: calc(3em / 4);
vertical-align: -15%;
}
.bwi-2x {
font-size: 2em;
}
.bwi-3x {
font-size: 3em;
}
.bwi-4x {
font-size: 4em;
}
// Spin Animations
.bwi-spin {
animation: bwi-spin 2s infinite linear;
}
@keyframes bwi-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
// List Icons
.bwi-ul {
padding-left: 0;
margin-left: calc(30em / 14);
list-style-type: none;
> li {
position: relative;
}
}
.bwi-li {
position: absolute;
left: calc(-30em / 14);
width: calc(30em / 14);
top: calc(2em / 14);
text-align: center;
&.bwi-lg {
left: calc(-30em / 14) + calc(4em / 14);
}
}
// Rotation
.bwi-rotate-270 {
transform: rotate(270deg);
}
// For new icons - add their glyph name and value to the map below
$icons: (
"save-changes": "\e988",
"browser": "\e985",
"mobile": "\e986",
"cli": "\e987",
"providers": "\e983",
"vault": "\e984",
"folder-closed-f": "\e982",
"rocket": "\e9ee",
"ellipsis-h": "\e9ef",
"ellipsis-v": "\e9f0",
"safari": "\e974",
"opera": "\e975",
"firefox": "\e976",
"edge": "\e977",
"chrome": "\e978",
"star-f": "\e979",
"arrow-circle-up": "\e97a",
"arrow-circle-right": "\e97b",
"arrow-circle-left": "\e97c",
"arrow-circle-down": "\e97d",
"undo": "\e97e",
"bolt": "\e97f",
"puzzle": "\e980",
"rss": "\e973",
"dbl-angle-left": "\e970",
"dbl-angle-right": "\e971",
"hamburger": "\e972",
"bw-folder-open-f": "\e93e",
"desktop": "\e96a",
"angle-left": "\e96b",
"user": "\e900",
"user-f": "\e901",
"key": "\e902",
"share-square": "\e903",
"hashtag": "\e904",
"clone": "\e905",
"list-alt": "\e906",
"id-card": "\e907",
"credit-card": "\e908",
"globe": "\e909",
"sticky-note": "\e90a",
"folder": "\e90b",
"lock": "\e90c",
"lock-f": "\e90d",
"generate": "\e90e",
"generate-f": "\e90f",
"cog": "\e910",
"cog-f": "\e911",
"check-circle": "\e912",
"eye": "\e913",
"pencil-square": "\e914",
"bookmark": "\e915",
"files": "\e916",
"trash": "\e917",
"plus": "\e918",
"star": "\e919",
"list": "\e91a",
"angle-right": "\e91b",
"external-link": "\e91c",
"refresh": "\e91d",
"search": "\e91f",
"filter": "\e920",
"plus-circle": "\e921",
"user-circle": "\e922",
"question-circle": "\e923",
"cogs": "\e924",
"minus-circle": "\e925",
"send": "\e926",
"send-f": "\e927",
"download": "\e928",
"pencil": "\e929",
"sign-out": "\e92a",
"share": "\e92b",
"clock": "\e92c",
"angle-down": "\e92d",
"caret-down": "\e92e",
"square": "\e92f",
"collection": "\e930",
"bank": "\e931",
"shield": "\e932",
"stop": "\e933",
"plus-square": "\e934",
"save": "\e935",
"sign-in": "\e936",
"spinner": "\e937",
"dollar": "\e939",
"check": "\e93a",
"check-square": "\e93b",
"minus-square": "\e93c",
"close": "\e93d",
"share-arrow": "\e96c",
"paperclip": "\e93f",
"bitcoin": "\e940",
"cut": "\e941",
"frown": "\e942",
"folder-open": "\e943",
"bug": "\e946",
"chain-broken": "\e947",
"dashboard": "\e948",
"envelope": "\e949",
"exclamation-circle": "\e94a",
"exclamation-triangle": "\e94b",
"caret-right": "\e94c",
"file-pdf": "\e94e",
"file-text": "\e94f",
"info-circle": "\e952",
"lightbulb": "\e953",
"link": "\e954",
"linux": "\e956",
"long-arrow-right": "\e957",
"money": "\e958",
"play": "\e959",
"reddit": "\e95a",
"refresh-tab": "\e95b",
"sitemap": "\e95c",
"sliders": "\e95d",
"tag": "\e95e",
"thumb-tack": "\e95f",
"thumbs-up": "\e960",
"unlock": "\e962",
"users": "\e963",
"wrench": "\e965",
"ban": "\e967",
"camera": "\e968",
"chevron-up": "\e969",
"eye-slash": "\e96d",
"file": "\e96e",
"paste": "\e96f",
"github": "\e950",
"facebook": "\e94d",
"paypal": "\e938",
"google": "\e951",
"linkedin": "\e955",
"discourse": "\e91e",
"twitter": "\e961",
"youtube": "\e966",
"windows": "\e964",
"apple": "\e945",
"android": "\e944",
"error": "\e981",
"numbered-list": "\e989",
);
@each $name, $glyph in $icons {
.bwi-#{$name}:before {
content: $glyph;
}
}

View File

@@ -1,44 +0,0 @@
$card-icons-base: "~@bitwarden/jslib-angular/src/images/cards/";
$card-icons: (
"visa": $card-icons-base + "visa-light.png",
"amex": $card-icons-base + "amex-light.png",
"diners-club": $card-icons-base + "diners_club-light.png",
"discover": $card-icons-base + "discover-light.png",
"jcb": $card-icons-base + "jcb-light.png",
"maestro": $card-icons-base + "maestro-light.png",
"mastercard": $card-icons-base + "mastercard-light.png",
"union-pay": $card-icons-base + "union_pay-light.png",
);
$card-icons-dark: (
"visa": $card-icons-base + "visa-dark.png",
"amex": $card-icons-base + "amex-dark.png",
"diners-club": $card-icons-base + "diners_club-dark.png",
"discover": $card-icons-base + "discover-dark.png",
"jcb": $card-icons-base + "jcb-dark.png",
"maestro": $card-icons-base + "maestro-dark.png",
"mastercard": $card-icons-base + "mastercard-dark.png",
"union-pay": $card-icons-base + "union_pay-dark.png",
);
.credit-card-icon {
display: block; // Resolves the parent container being slighly to big
height: 19px;
width: 24px;
background-size: contain;
background-repeat: no-repeat;
}
@each $name, $url in $card-icons {
.card-#{$name} {
background-image: url("#{$url}");
}
}
@each $theme in $dark-icon-themes {
@each $name, $url in $card-icons-dark {
.#{$theme} .card-#{$name} {
background-image: url("#{$url}");
}
}
}

View File

@@ -1,89 +0,0 @@
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 300;
font-display: auto;
src: url(webfonts/Open_Sans-italic-300.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400;
font-display: auto;
src: url(webfonts/Open_Sans-italic-400.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 600;
font-display: auto;
src: url(webfonts/Open_Sans-italic-600.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 700;
font-display: auto;
src: url(webfonts/Open_Sans-italic-700.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 800;
font-display: auto;
src: url(webfonts/Open_Sans-italic-800.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 300;
font-display: auto;
src: url(webfonts/Open_Sans-normal-300.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400;
font-display: auto;
src: url(webfonts/Open_Sans-normal-400.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 600;
font-display: auto;
src: url(webfonts/Open_Sans-normal-600.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 700;
font-display: auto;
src: url(webfonts/Open_Sans-normal-700.woff) format("woff");
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 800;
font-display: auto;
src: url(webfonts/Open_Sans-normal-800.woff) format("woff");
unicode-range: U+0-10FFFF;
}

View File

@@ -1,6 +0,0 @@
import { Injectable } from "@angular/core";
import { BroadcasterService as BaseBroadcasterService } from "@/jslib/common/src/services/broadcaster.service";
@Injectable()
export class BroadcasterService extends BaseBroadcasterService {}

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

@@ -1,180 +0,0 @@
import {
ApplicationRef,
ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
EmbeddedViewRef,
Injectable,
Injector,
Type,
ViewContainerRef,
} from "@angular/core";
import { first, firstValueFrom } from "rxjs";
import { DynamicModalComponent } from "../components/modal/dynamic-modal.component";
import { ModalInjector } from "../components/modal/modal-injector";
import { ModalRef } from "../components/modal/modal.ref";
export class ModalConfig<D = any> {
data?: D;
allowMultipleModals = false;
}
@Injectable()
export class ModalService {
protected modalList: ComponentRef<DynamicModalComponent>[] = [];
// Lazy loaded modules are not available in componentFactoryResolver,
// therefore modules needs to manually initialize their resolvers.
private factoryResolvers: Map<Type<any>, ComponentFactoryResolver> = new Map();
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private applicationRef: ApplicationRef,
private injector: Injector,
) {
document.addEventListener("keyup", (event) => {
if (event.key === "Escape" && this.modalCount > 0) {
this.topModal.instance.close();
}
});
}
get modalCount() {
return this.modalList.length;
}
private get topModal() {
return this.modalList[this.modalCount - 1];
}
async openViewRef<T>(
componentType: Type<T>,
viewContainerRef: ViewContainerRef,
setComponentParameters: (component: T) => void = null,
): Promise<[ModalRef, T]> {
const [modalRef, modalComponentRef] = this.openInternal(componentType, null, false);
modalComponentRef.instance.setComponentParameters = setComponentParameters;
viewContainerRef.insert(modalComponentRef.hostView);
await firstValueFrom(modalRef.onCreated);
return [modalRef, modalComponentRef.instance.componentRef.instance];
}
open(componentType: Type<any>, config?: ModalConfig) {
if (!(config?.allowMultipleModals ?? false) && this.modalCount > 0) {
return;
}
// eslint-disable-next-line
const [modalRef, _] = this.openInternal(componentType, config, true);
return modalRef;
}
registerComponentFactoryResolver<T>(
componentType: Type<T>,
componentFactoryResolver: ComponentFactoryResolver,
): void {
this.factoryResolvers.set(componentType, componentFactoryResolver);
}
resolveComponentFactory<T>(componentType: Type<T>): ComponentFactory<T> {
if (this.factoryResolvers.has(componentType)) {
return this.factoryResolvers.get(componentType).resolveComponentFactory(componentType);
}
return this.componentFactoryResolver.resolveComponentFactory(componentType);
}
protected openInternal(
componentType: Type<any>,
config?: ModalConfig,
attachToDom?: boolean,
): [ModalRef, ComponentRef<DynamicModalComponent>] {
const [modalRef, componentRef] = this.createModalComponent(config);
componentRef.instance.childComponentType = componentType;
if (attachToDom) {
this.applicationRef.attachView(componentRef.hostView);
const domElem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
document.body.appendChild(domElem);
}
modalRef.onClosed.pipe(first()).subscribe(() => {
if (attachToDom) {
this.applicationRef.detachView(componentRef.hostView);
}
componentRef.destroy();
this.modalList.pop();
if (this.modalCount > 0) {
this.topModal.instance.getFocus();
}
});
this.setupHandlers(modalRef);
this.modalList.push(componentRef);
return [modalRef, componentRef];
}
protected setupHandlers(modalRef: ModalRef) {
let backdrop: HTMLElement = null;
// Add backdrop, setup [data-dismiss] handler.
modalRef.onCreated.pipe(first()).subscribe((el) => {
document.body.classList.add("modal-open");
const modalEl: HTMLElement = el.querySelector(".modal");
const dialogEl = modalEl.querySelector(".modal-dialog") as HTMLElement;
backdrop = document.createElement("div");
backdrop.className = "modal-backdrop fade";
backdrop.style.zIndex = `${this.modalCount}040`;
modalEl.prepend(backdrop);
dialogEl.addEventListener("click", (e: Event) => {
e.stopPropagation();
});
dialogEl.style.zIndex = `${this.modalCount}050`;
const modals = Array.from(
el.querySelectorAll('.modal-backdrop, .modal *[data-bs-dismiss="modal"]'),
);
for (const closeElement of modals) {
closeElement.addEventListener("click", () => {
modalRef.close();
});
}
});
// onClose is used in Web to hook into bootstrap. On other projects we pipe it directly to closed.
modalRef.onClose.pipe(first()).subscribe(() => {
modalRef.closed();
if (this.modalCount === 0) {
document.body.classList.remove("modal-open");
}
});
}
protected createModalComponent(
config: ModalConfig,
): [ModalRef, ComponentRef<DynamicModalComponent>] {
const modalRef = new ModalRef();
const map = new WeakMap();
map.set(ModalConfig, config);
map.set(ModalRef, modalRef);
const componentFactory =
this.componentFactoryResolver.resolveComponentFactory(DynamicModalComponent);
const componentRef = componentFactory.create(new ModalInjector(this.injector, map));
return [modalRef, componentRef];
}
}

View File

@@ -1,38 +0,0 @@
import { Injectable } from "@angular/core";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
@Injectable()
export class ValidationService {
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
) {}
showError(data: any): string[] {
const defaultErrorMessage = this.i18nService.t("unexpectedError");
let errors: string[] = [];
if (data != null && typeof data === "string") {
errors.push(data);
} else if (data == null || typeof data !== "object") {
errors.push(defaultErrorMessage);
} else if (data.validationErrors != null) {
errors = errors.concat((data as ErrorResponse).getAllMessages());
} else {
errors.push(data.message ? data.message : defaultErrorMessage);
}
if (errors.length === 1) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errors[0]);
} else if (errors.length > 1) {
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errors, {
timeout: 5000 * errors.length,
});
}
return errors;
}
}

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).toThrow("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).toThrow("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,99 +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 () {
interceptions.log = arguments;
},
warn: function () {
interceptions.warn = arguments;
},
error: function () {
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,5 +0,0 @@
import { webcrypto } from "crypto";
Object.defineProperty(window, "crypto", {
value: webcrypto,
});

View File

@@ -1,26 +0,0 @@
function newGuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
export function GetUniqueString(prefix = "") {
return prefix + "_" + newGuid();
}
export function BuildTestObject<T, K extends keyof T = keyof T>(
def: Partial<Pick<T, K>> | T,
constructor?: new () => T,
): T {
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
}
export function makeStaticByteArray(length: number, start = 0) {
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = start + i;
}
return arr.buffer;
}

View File

@@ -1,14 +0,0 @@
import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
import { PasswordTokenRequest } from "../models/request/identityToken/passwordTokenRequest";
import { SsoTokenRequest } from "../models/request/identityToken/ssoTokenRequest";
import { OrganizationImportRequest } from "../models/request/organizationImportRequest";
import { IdentityCaptchaResponse } from "../models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
export abstract class ApiService {
postIdentityToken: (
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest,
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
postPublicImportDirectory: (request: OrganizationImportRequest) => Promise<any>;
}

View File

@@ -1,4 +0,0 @@
export abstract class AppIdService {
getAppId: () => Promise<string>;
getAnonymousAppId: () => Promise<string>;
}

View File

@@ -1,5 +0,0 @@
export abstract class BroadcasterService {
send: (message: any, id?: string) => void;
subscribe: (id: string, messageCallback: (message: any) => any) => void;
unsubscribe: (id: string) => void;
}

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,62 +0,0 @@
import { DecryptParameters } from "../models/domain/decryptParameters";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
export abstract class CryptoFunctionService {
pbkdf2: (
password: string | ArrayBuffer,
salt: string | ArrayBuffer,
algorithm: "sha256" | "sha512",
iterations: number,
) => Promise<ArrayBuffer>;
hkdf: (
ikm: ArrayBuffer,
salt: string | ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512",
) => Promise<ArrayBuffer>;
hkdfExpand: (
prk: ArrayBuffer,
info: string | ArrayBuffer,
outputByteSize: number,
algorithm: "sha256" | "sha512",
) => Promise<ArrayBuffer>;
hash: (
value: string | ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512" | "md5",
) => Promise<ArrayBuffer>;
hmac: (
value: ArrayBuffer,
key: ArrayBuffer,
algorithm: "sha1" | "sha256" | "sha512",
) => Promise<ArrayBuffer>;
compare: (a: ArrayBuffer, b: ArrayBuffer) => Promise<boolean>;
hmacFast: (
value: ArrayBuffer | string,
key: ArrayBuffer | string,
algorithm: "sha1" | "sha256" | "sha512",
) => Promise<ArrayBuffer | string>;
compareFast: (a: ArrayBuffer | string, b: ArrayBuffer | string) => Promise<boolean>;
aesEncrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
aesDecryptFastParameters: (
data: string,
iv: string,
mac: string,
key: SymmetricCryptoKey,
) => DecryptParameters<ArrayBuffer | string>;
aesDecryptFast: (parameters: DecryptParameters<ArrayBuffer | string>) => Promise<string>;
aesDecrypt: (data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer) => Promise<ArrayBuffer>;
rsaEncrypt: (
data: ArrayBuffer,
publicKey: ArrayBuffer,
algorithm: "sha1" | "sha256",
) => Promise<ArrayBuffer>;
rsaDecrypt: (
data: ArrayBuffer,
privateKey: ArrayBuffer,
algorithm: "sha1" | "sha256",
) => Promise<ArrayBuffer>;
rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise<ArrayBuffer>;
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[ArrayBuffer, ArrayBuffer]>;
randomBytes: (length: number) => Promise<ArrayBuffer>;
}

View File

@@ -1,34 +0,0 @@
import { Observable } from "rxjs";
export type Urls = {
base?: string;
webVault?: string;
api?: string;
identity?: string;
icons?: string;
notifications?: string;
events?: string;
keyConnector?: string;
};
export type PayPalConfig = {
businessId?: string;
buttonAction?: string;
};
export abstract class EnvironmentService {
urls: Observable<Urls>;
hasBaseUrl: () => boolean;
getNotificationsUrl: () => string;
getWebVaultUrl: () => string;
getSendUrl: () => string;
getIconsUrl: () => string;
getApiUrl: () => string;
getIdentityUrl: () => string;
getEventsUrl: () => string;
getKeyConnectorUrl: () => string;
setUrlsFromStorage: () => Promise<void>;
setUrls: (urls: Urls) => Promise<Urls>;
getUrls: () => Urls;
}

View File

@@ -1,9 +0,0 @@
export abstract class I18nService {
locale: string;
supportedTranslationLocales: string[];
translationLocale: string;
collator: Intl.Collator;
localeNames: Map<string, string>;
t: (id: string, p1?: string, p2?: string, p3?: string) => string;
translate: (id: string, p1?: string, p2?: string, p3?: string) => string;
}

View File

@@ -1,11 +0,0 @@
import { LogLevelType } from "../enums/logLevelType";
export abstract class LogService {
debug: (message: string) => void;
info: (message: string) => void;
warning: (message: string) => void;
error: (message: string) => void;
write: (level: LogLevelType, message: string) => void;
time: (label: string) => void;
timeEnd: (label: string) => [number, number];
}

View File

@@ -1,3 +0,0 @@
export abstract class MessagingService {
send: (subscriber: string, arg?: any) => void;
}

View File

@@ -1,52 +0,0 @@
import { ClientType } from "../enums/clientType";
import { DeviceType } from "../enums/deviceType";
import { ThemeType } from "../enums/themeType";
interface ToastOptions {
timeout?: number;
}
export abstract class PlatformUtilsService {
getDevice: () => DeviceType;
getDeviceString: () => string;
getClientType: () => ClientType;
isFirefox: () => boolean;
isChrome: () => boolean;
isEdge: () => boolean;
isOpera: () => boolean;
isVivaldi: () => boolean;
isSafari: () => boolean;
isMacAppStore: () => boolean;
isViewOpen: () => Promise<boolean>;
launchUri: (uri: string, options?: any) => void;
saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void;
getApplicationVersion: () => Promise<string>;
supportsWebAuthn: (win: Window) => boolean;
supportsDuo: () => boolean;
showToast: (
type: "error" | "success" | "warning" | "info",
title: string,
text: string | string[],
options?: ToastOptions,
) => void;
showDialog: (
body: string,
title?: string,
confirmText?: string,
cancelText?: string,
type?: string,
bodyIsHtml?: boolean,
) => Promise<boolean>;
isDev: () => boolean;
isSelfHost: () => boolean;
copyToClipboard: (text: string, options?: any) => void | boolean;
readFromClipboard: (options?: any) => Promise<string>;
supportsBiometric: () => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>;
getDefaultSystemTheme: () => Promise<ThemeType.Light | ThemeType.Dark>;
onDefaultSystemThemeChange: (
callback: (theme: ThemeType.Light | ThemeType.Dark) => unknown,
) => unknown;
getEffectiveTheme: () => Promise<ThemeType>;
supportsSecureStorage: () => boolean;
}

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
import { StorageOptions } from "../models/domain/storageOptions";
export abstract class StorageService {
get: <T>(key: string, options?: StorageOptions) => Promise<T>;
has: (key: string, options?: StorageOptions) => Promise<boolean>;
save: (key: string, obj: any, options?: StorageOptions) => Promise<any>;
remove: (key: string, options?: StorageOptions) => Promise<any>;
}

View File

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

View File

@@ -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,8 +0,0 @@
export enum ClientType {
Web = "web",
Browser = "browser",
Desktop = "desktop",
Mobile = "mobile",
Cli = "cli",
DirectoryConnector = "connector",
}

View File

@@ -1,23 +0,0 @@
export enum DeviceType {
Android = 0,
iOS = 1,
ChromeExtension = 2,
FirefoxExtension = 3,
OperaExtension = 4,
EdgeExtension = 5,
WindowsDesktop = 6,
MacOsDesktop = 7,
LinuxDesktop = 8,
ChromeBrowser = 9,
FirefoxBrowser = 10,
OperaBrowser = 11,
EdgeBrowser = 12,
IEBrowser = 13,
UnknownBrowser = 14,
AndroidAmazon = 15,
UWP = 16,
SafariBrowser = 17,
VivaldiBrowser = 18,
VivaldiExtension = 19,
SafariExtension = 20,
}

View File

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

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