1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-02-24 16:43:06 +00:00

Compare commits

..

67 Commits

Author SHA1 Message Date
JaredScar
3abd3f0496 Refactor secure storage implementation to use native bindings and migrate from keytar. Update .gitignore for Rust artifacts, adjust package.json for new build scripts, and modify workflows for native module compilation. Enhance state versioning to support migration of credentials from keytar to desktop_core. 2026-02-24 11:42:16 -05:00
renovate[bot]
af430157e0 [deps]: Update minimatch to v10 [SECURITY] - abandoned (#1009)
* [deps]: Update minimatch to v10 [SECURITY]

* Remove erroneous failing dependencies

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Sven <svernyi@bitwarden.com>
2026-02-23 13:10:23 -06:00
Jared
db3e7aa685 Refactor error handling in LdapDirectoryService to ensure proper unbinding and error propagation (#995) 2026-02-23 12:42:39 -05:00
Brandon Treston
9a2168c1d7 add lint workflow (#1006) 2026-02-19 11:43:39 -05:00
renovate[bot]
1fd8bf318f [deps]: Update webpack to v5.105.1 (#999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-17 11:49:12 -05:00
renovate[bot]
c472d5e199 [deps]: Lock file maintenance (#1001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jared <TheWolfBadger@gmail.com>
2026-02-17 11:04:46 -05:00
renovate[bot]
1a42e76c79 [deps]: Update eslint-plugin-rxjs-x to v0.9.1 (#998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-16 15:47:22 +00:00
Mick Letofsky
1aad9e1cbe Slim down and align with our current practices (#994) 2026-02-11 15:58:42 +01:00
Brandon Treston
3059934d4c remove substitute (#992) 2026-02-10 09:41:26 -05:00
Vincent Salucci
42cf13df08 chore: bump version to 2026.2.0 (#993) 2026-02-09 14:11:35 -06:00
renovate[bot]
1a9f0a2ca7 [deps]: Update babel-loader to v10 (#987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 15:08:41 +00:00
renovate[bot]
30b3595de3 [deps]: Update typescript-eslint monorepo to v8.54.0 (#976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 14:34:42 +00:00
renovate[bot]
28f0ff4b24 [deps]: Update angular-cli monorepo to v21.1.2 (#982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 10:11:28 -06:00
renovate[bot]
14fc69c810 [deps]: Update ngx-toastr to v20 (#989)
* [deps]: Update ngx-toastr to v20

* Adjust to toastr v20

---------

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

* add COEP and COOP headers to enabled SharedArrayBuffer

---------

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

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

* ng update 21 wip

* update @angular/cdk@21

* NG 21 WIP

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

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

* add missing polyfils for TextEncoder & TextDecoder

* cleanup lock file

* tsconfig cleanup

* fix import

* cleanup

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

* Upgrade all Angular packages

* Downgrade jest-mock-extended to support Jest 29

---------

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

* Updated return types.

---------

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

* resolve lint errors, upgrade eslint, replace unmaintained packages

* refresh lockfile

---------

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

* chore: npm install to update package-lock
2025-12-15 13:44:21 -06:00
renovate[bot]
9a719c9e4e [deps]: Update glob to v13 (#950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 14:35:36 -06:00
renovate[bot]
2f49f4d5f1 [deps]: Update jest-mock-extended to v4 (#868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-12 14:42:43 +00:00
renovate[bot]
5426f251a7 [deps]: Update webpack to v5.103.0 (#943)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 14:53:08 -05:00
renovate[bot]
a0c30350d4 [deps]: Update sass to v1.94.2 (#935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 18:00:14 +00:00
renovate[bot]
6f3d8f73e1 [deps]: Update actions/checkout action to v6 (#946)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 14:13:24 +00:00
renovate[bot]
d7be5486c7 [deps]: Update prettier to v3.7.4 (#941)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 14:09:43 +00:00
gitclonebrian
ce43f651ab added permissions to token generation step to limit scope of token (#929) 2025-12-10 17:46:23 -05:00
renovate[bot]
eda713bcc9 [deps]: Update type-fest to v5.3.0 (#907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 09:55:29 -05:00
renovate[bot]
b53e145e62 [deps]: Update typescript-eslint monorepo to v8.48.0 (#942)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-09 08:53:34 +10:00
renovate[bot]
2ad35be82e [deps]: Update @angular/compiler to v20.3.15 [SECURITY] (#939)
* [deps]: Update @angular/compiler to v20.3.15 [SECURITY]

* Upgrade Angular packages to v20.3.15

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Rui Tome <rtome@bitwarden.com>
2025-12-02 15:57:14 +00:00
renovate[bot]
bdfc8ae5eb [deps]: Update node-forge to v1.3.2 [SECURITY] (#937)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-01 11:08:58 -05:00
renovate[bot]
7d218eac2f [deps]: Update @angular/common to v20.3.14 [SECURITY] (#938)
* [deps]: Update @angular/common to v20.3.14 [SECURITY]

* Upgrade all @angular packages to 20.3.14

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2025-11-28 10:52:30 +10:00
renovate[bot]
ccbb24d504 [deps]: Update rimraf to v6.1.0 (#914)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 15:34:31 -05:00
renovate[bot]
dd1f36e3d6 [deps]: Update electron to v39.2.1 (#934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 14:53:45 -05:00
Vincent Salucci
0780f9a931 chore: change checkboxes to dropdowns (#932) 2025-11-21 14:39:23 -06:00
Vincent Salucci
62c8a64298 chore: attempt to fix checkboxes required state (#930) 2025-11-21 13:45:22 -06:00
brandonbiete
0d3bbc1db8 [BRE-1367] Update macos workflows to use macos-15-intel runners (#928) 2025-11-21 14:38:59 -05:00
Vincent Salucci
99655a0abf chore: add issue template and base config (#926)
* chore: add issue template and base config

* chore: add additional details to application type and add additional directory service

* chore: group LDAP services
2025-11-20 20:24:22 -06:00
brandonbiete
2883ff6068 [BRE-1302] Revert runner upgrade and target arch changes to get back to stable state (#925) 2025-11-20 11:06:49 -05:00
renovate[bot]
f5abaf114a [deps]: Update actions/setup-node action to v6 (#908)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 15:22:43 +00:00
renovate[bot]
5792578946 [deps]: Update glob to v11.1.0 [SECURITY] (#923)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 09:48:22 -05:00
renovate[bot]
6b3b29a1a0 [deps]: Update angular-eslint monorepo to v20.6.0 (#911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 14:06:00 +00:00
renovate[bot]
02809be178 [deps]: Update actions/upload-artifact action to v5 (#916)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 13:43:59 +00:00
Thomas Rittson
6abfdd8a88 Fix not reporting test results on push to main (#921)
Allow other events to report test results
2025-11-18 08:50:19 +10:00
75 changed files with 10040 additions and 9419 deletions

View File

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

View File

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

View File

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

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

111
.github/ISSUE_TEMPLATE/issue.yml vendored Normal file
View File

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

View File

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

View File

@@ -23,7 +23,7 @@ jobs:
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -51,42 +51,36 @@ jobs:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
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
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Set up system dependencies
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
sudo apt-get update
sudo apt-get -y install libdbus-1-dev libsecret-1-dev pkg-config
- name: Install
run: npm install
- name: Build native module
run: npm run build:native:release
- 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"
run: zip -j "dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip" "dist-cli/linux/bwdc" "node_modules/dc-native/dc_native.linux-x64-gnu.node"
- name: Version Test
run: |
@@ -111,7 +105,7 @@ jobs:
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
@@ -120,7 +114,7 @@ jobs:
macos-cli:
name: Build Mac CLI
runs-on: macos-13
runs-on: macos-15-intel
needs: setup
permissions:
contents: read
@@ -129,42 +123,31 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
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: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Install
run: npm install
- name: Build native module
run: npm run build:native:release
- 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"
run: zip -j "dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip" "dist-cli/macos/bwdc" "node_modules/dc-native/dc_native.darwin-x64.node"
- name: Version Test
run: |
@@ -182,7 +165,7 @@ jobs:
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
@@ -200,7 +183,7 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -209,42 +192,29 @@ jobs:
choco install checksum --no-progress
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
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
shell: pwsh
run: |
$keytarVersion = (Get-Content -Raw -Path ./package.json | ConvertFrom-Json).dependencies.keytar
$keytarTar = "keytar-v${keytarVersion}-napi-v3-{0}-x64.tar"
$keytarTarGz = "${keytarTar}.gz"
$keytarUrl = "https://github.com/atom/node-keytar/releases/download/v${keytarVersion}/${keytarTarGz}"
New-Item -ItemType directory -Path ./keytar/windows | Out-Null
Invoke-RestMethod -Uri $($keytarUrl -f "win32") -OutFile "./keytar/windows/$($keytarTarGz -f "win32")"
7z e "./keytar/windows/$($keytarTarGz -f "win32")" -o"./keytar/windows"
7z e "./keytar/windows/$($keytarTar -f "win32")" -o"./keytar/windows"
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Install
run: npm install
- name: Build native module
run: npm run build:native:release
- name: Package CLI
run: npm run dist:cli:win
- 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 .\node_modules\dc-native\dc_native.win32-x64-msvc.node
- name: Version Test
shell: pwsh
@@ -258,7 +228,7 @@ jobs:
}
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
@@ -279,21 +249,21 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
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: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Print environment
run: |
@@ -338,28 +308,28 @@ jobs:
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest.yml
path: ./dist/latest.yml
@@ -379,26 +349,24 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
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: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev libdbus-1-dev
sudo apt-get -y install rpm
- name: NPM Install
@@ -411,14 +379,14 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -427,7 +395,7 @@ jobs:
macos-gui:
name: Build MacOS GUI
runs-on: macos-13
runs-on: macos-15-intel
needs: setup
permissions:
contents: read
@@ -439,21 +407,19 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
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: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Print environment
run: |
@@ -542,28 +508,28 @@ jobs:
CSC_FOR_PULL_REQUEST: true
- name: Upload .zip artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml

View File

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

46
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Lint
on:
workflow_dispatch:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
pull_request:
permissions:
contents: read
jobs:
lint:
name: Run linter
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
- name: Run ESLint and Prettier
run: npm run lint

View File

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

View File

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

View File

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

View File

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

4
.gitignore vendored
View File

@@ -32,6 +32,10 @@ build
build-cli
.angular/cache
# Rust build artifacts
native/target
native/*.node
# Testing
coverage*
junit.xml*

2
.nvmrc
View File

@@ -1 +1 @@
v20
v22

View File

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

View File

@@ -4,13 +4,14 @@
},
"productName": "Bitwarden Directory Connector",
"appId": "com.bitwarden.directory-connector",
"copyright": "Copyright © 2015-2022 Bitwarden Inc.",
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
"directories": {
"buildResources": "resources",
"output": "dist",
"app": "build"
},
"afterSign": "scripts/notarize.js",
"asarUnpack": ["node_modules/dc-native/*.node"],
"mac": {
"artifactName": "Bitwarden-Connector-${version}-mac.${ext}",
"category": "public.app-category.productivity",

150
eslint.config.mjs Normal file
View File

@@ -0,0 +1,150 @@
// @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/**",
"native/**",
],
},
// 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

@@ -26,7 +26,6 @@ module.exports = {
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: "<rootDir>/" }),
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
// Workaround for a memory leak that crashes tests in CI:
// https://github.com/facebook/jest/issues/9430#issuecomment-1149882002
// Also anecdotally improves performance when run locally

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ describe("SymmetricCryptoKey", () => {
new SymmetricCryptoKey(null);
};
expect(t).toThrowError("Must provide key");
expect(t).toThrow("Must provide key");
});
describe("guesses encKey from key length", () => {
@@ -63,7 +63,7 @@ describe("SymmetricCryptoKey", () => {
new SymmetricCryptoKey(makeStaticByteArray(30));
};
expect(t).toThrowError("Unable to determine encType.");
expect(t).toThrow("Unable to determine encType.");
});
});
});

View File

@@ -8,15 +8,12 @@ declare let console: any;
export function interceptConsole(interceptions: any): object {
console = {
log: function () {
// eslint-disable-next-line
interceptions.log = arguments;
},
warn: function () {
// eslint-disable-next-line
interceptions.warn = arguments;
},
error: function () {
// eslint-disable-next-line
interceptions.error = arguments;
},
};

View File

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

View File

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

View File

@@ -3,5 +3,6 @@ export enum StateVersion {
Two = 2, // Move to a typed State object
Three = 3, // Fix migration of users' premium status
Four = 4, // Fix 'Never Lock' option by removing stale data
Latest = Four,
Five = 5, // Migrate Windows keychain credentials from keytar (UTF-8) to desktop_core (UTF-16)
Latest = Five,
}

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ export class EncString {
try {
this.encryptionType = parseInt(headerPieces[0], null);
encPieces = headerPieces[1].split("|");
} catch (e) {
} catch {
return;
}
} else {
@@ -114,7 +114,7 @@ export class EncString {
key = await cryptoService.getOrgKey(orgId);
}
this.decryptedValue = await cryptoService.decryptToUtf8(this, key);
} catch (e) {
} catch {
this.decryptedValue = "[error: cannot decrypt]";
}
return this.decryptedValue;

View File

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

View File

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

View File

@@ -335,9 +335,11 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async clearStoredKey(keySuffix: KeySuffixOptions) {
keySuffix === KeySuffixOptions.Auto
? await this.stateService.setCryptoMasterKeyAuto(null)
: await this.stateService.setCryptoMasterKeyBiometric(null);
if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null);
} else {
await this.stateService.setCryptoMasterKeyBiometric(null);
}
}
async clearKeyHash(userId?: string): Promise<any> {
@@ -634,9 +636,9 @@ export class CryptoService implements CryptoServiceAbstraction {
const encBytes = new Uint8Array(encBuf);
const encType = encBytes[0];
let ctBytes: Uint8Array = null;
let ivBytes: Uint8Array = null;
let macBytes: Uint8Array = null;
let ctBytes: Uint8Array<ArrayBuffer> = null;
let ivBytes: Uint8Array<ArrayBuffer> = null;
let macBytes: Uint8Array<ArrayBuffer> = null;
switch (encType) {
case EncryptionType.AesCbc128_HmacSha256_B64:
@@ -717,7 +719,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const privateKey = await this.decryptToBytes(new EncString(encPrivateKey), encKey);
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
} catch (e) {
} catch {
return false;
}

View File

@@ -38,8 +38,7 @@ const partialKeys = {
export class StateService<
TGlobalState extends GlobalState = GlobalState,
TAccount extends Account = Account,
> implements StateServiceAbstraction<TAccount>
{
> implements StateServiceAbstraction<TAccount> {
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
accounts$ = this.accountsSubject.asObservable();

View File

@@ -5,7 +5,7 @@ import { StorageOptions } from "@/jslib/common/src/models/domain/storageOptions"
export class ElectronRendererSecureStorageService implements StorageService {
async get<T>(key: string, options?: StorageOptions): Promise<T> {
const val = ipcRenderer.sendSync("keytar", {
const val = ipcRenderer.sendSync("nativeSecureStorage", {
action: "getPassword",
key: key,
keySuffix: options?.keySuffix ?? "",
@@ -14,7 +14,7 @@ export class ElectronRendererSecureStorageService implements StorageService {
}
async has(key: string, options?: StorageOptions): Promise<boolean> {
const val = ipcRenderer.sendSync("keytar", {
const val = ipcRenderer.sendSync("nativeSecureStorage", {
action: "hasPassword",
key: key,
keySuffix: options?.keySuffix ?? "",
@@ -23,7 +23,7 @@ export class ElectronRendererSecureStorageService implements StorageService {
}
async save(key: string, obj: any, options?: StorageOptions): Promise<any> {
ipcRenderer.sendSync("keytar", {
ipcRenderer.sendSync("nativeSecureStorage", {
action: "setPassword",
key: key,
keySuffix: options?.keySuffix ?? "",
@@ -33,7 +33,7 @@ export class ElectronRendererSecureStorageService implements StorageService {
}
async remove(key: string, options?: StorageOptions): Promise<any> {
ipcRenderer.sendSync("keytar", {
ipcRenderer.sendSync("nativeSecureStorage", {
action: "deletePassword",
key: key,
keySuffix: options?.keySuffix ?? "",

View File

@@ -1,6 +1,14 @@
import * as path from "path";
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, nativeImage, Tray } from "electron";
import {
app,
BrowserWindow,
Menu,
MenuItemConstructorOptions,
NativeImage,
nativeImage,
Tray,
} from "electron";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
@@ -12,8 +20,8 @@ export class TrayMain {
private appName: string;
private tray: Tray;
private icon: string | Electron.NativeImage;
private pressedIcon: Electron.NativeImage;
private icon: string | NativeImage;
private pressedIcon: NativeImage;
constructor(
private windowMain: WindowMain,

View File

@@ -1,7 +1,7 @@
import * as path from "path";
import * as url from "url";
import { app, BrowserWindow, screen } from "electron";
import { app, BrowserWindow, Rectangle, screen } from "electron";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
@@ -14,7 +14,7 @@ export class WindowMain {
win: BrowserWindow;
isQuitting = false;
private windowStateChangeTimer: NodeJS.Timeout;
private windowStateChangeTimer: ReturnType<typeof setTimeout>;
private windowStates: { [key: string]: any } = {};
private enableAlwaysOnTop = false;
@@ -37,7 +37,6 @@ export class WindowMain {
app.quit();
return;
} else {
// eslint-disable-next-line
app.on("second-instance", (event, argv, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (this.win != null) {
@@ -128,6 +127,13 @@ export class WindowMain {
},
});
// Enable SharedArrayBuffer. See https://developer.chrome.com/blog/enabling-shared-array-buffer/#cross-origin-isolation
this.win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
details.responseHeaders["Cross-Origin-Opener-Policy"] = ["same-origin"];
details.responseHeaders["Cross-Origin-Embedder-Policy"] = ["require-corp"];
callback({ responseHeaders: details.responseHeaders });
});
if (this.windowStates[mainWindowSizeKey].isMaximized) {
this.win.maximize();
}
@@ -241,7 +247,7 @@ export class WindowMain {
const state = await this.stateService.getWindow();
const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized);
let displayBounds: Electron.Rectangle = null;
let displayBounds: Rectangle = null;
if (!isValid) {
state.width = defaultWidth;
state.height = defaultHeight;

View File

@@ -94,7 +94,7 @@ describe("NodeCrypto Function Service", () => {
it("should fail with prk too small", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const f = cryptoFunctionService.hkdfExpand(
Utils.fromB64ToArray(prk16Byte),
Utils.fromB64ToArray(prk16Byte).buffer,
"info",
32,
"sha256",
@@ -105,7 +105,7 @@ describe("NodeCrypto Function Service", () => {
it("should fail with outputByteSize is too large", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const f = cryptoFunctionService.hkdfExpand(
Utils.fromB64ToArray(prk32Byte),
Utils.fromB64ToArray(prk32Byte).buffer,
"info",
8161,
"sha256",
@@ -341,7 +341,7 @@ function testHkdf(
utf8Key: string,
unicodeKey: string,
) {
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==");
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==").buffer;
const regularSalt = "salt";
const utf8Salt = "üser_salt";
@@ -393,7 +393,7 @@ function testHkdfExpand(
it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const okm = await cryptoFunctionService.hkdfExpand(
Utils.fromB64ToArray(b64prk),
Utils.fromB64ToArray(b64prk).buffer,
info,
outputByteSize,
algorithm,

3498
native/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
native/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "dc_native"
version = "0.1.0"
edition = "2021"
description = "Native keychain bindings for Bitwarden Directory Connector"
license = "GPL-3.0"
[lib]
crate-type = ["cdylib"]
name = "dc_native"
[dependencies]
anyhow = "=1.0.100"
desktop_core = { git = "https://github.com/bitwarden/clients", rev = "00cf24972d944638bbd1adc00a0ae3eeabb6eb9a", package = "desktop_core" }
napi = { version = "=3.3.0", features = ["async"] }
napi-derive = "=3.2.5"
[target.'cfg(windows)'.dependencies]
scopeguard = "=1.2.0"
widestring = "=1.2.0"
windows = { version = "=0.61.1", features = [
"Win32_Foundation",
"Win32_Security_Credentials",
] }
[build-dependencies]
napi-build = "=2.2.3"

5
native/build.rs Normal file
View File

@@ -0,0 +1,5 @@
extern crate napi_build;
fn main() {
napi_build::setup();
}

34
native/index.d.ts vendored Normal file
View File

@@ -0,0 +1,34 @@
export declare namespace passwords {
/** The error message returned when a password is not found during retrieval or deletion. */
export const PASSWORD_NOT_FOUND: string;
/**
* Fetch the stored password from the keychain.
* Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
*/
export function getPassword(service: string, account: string): Promise<string>;
/**
* Save the password to the keychain. Adds an entry if none exists, otherwise updates it.
*/
export function setPassword(service: string, account: string, password: string): Promise<void>;
/**
* Delete the stored password from the keychain.
* Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
*/
export function deletePassword(service: string, account: string): Promise<void>;
/**
* Check if OS secure storage is available.
*/
export function isAvailable(): Promise<boolean>;
/**
* Migrate a credential previously stored by keytar (UTF-8 blob on Windows) to the UTF-16
* format used by desktop_core. No-ops on non-Windows platforms.
*
* Returns true if a migration was performed, false otherwise.
*/
export function migrateKeytarPassword(service: string, account: string): Promise<boolean>;
}

67
native/index.js Normal file
View File

@@ -0,0 +1,67 @@
const { existsSync } = require("fs");
const { join } = require("path");
const { platform, arch } = process;
let nativeBinding = null;
let loadError = null;
function loadFirstAvailable(localFiles) {
for (const localFile of localFiles) {
const filePath = join(__dirname, localFile);
if (existsSync(filePath)) {
return require(filePath);
}
}
throw new Error(`Could not find dc-native binary. Run 'npm run build:native' to compile it.`);
}
switch (platform) {
case "win32":
switch (arch) {
case "x64":
nativeBinding = loadFirstAvailable(["dc_native.win32-x64-msvc.node"]);
break;
case "arm64":
nativeBinding = loadFirstAvailable(["dc_native.win32-arm64-msvc.node"]);
break;
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`);
}
break;
case "darwin":
switch (arch) {
case "x64":
nativeBinding = loadFirstAvailable(["dc_native.darwin-x64.node"]);
break;
case "arm64":
nativeBinding = loadFirstAvailable(["dc_native.darwin-arm64.node"]);
break;
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`);
}
break;
case "linux":
switch (arch) {
case "x64":
nativeBinding = loadFirstAvailable(["dc_native.linux-x64-gnu.node"]);
break;
case "arm64":
nativeBinding = loadFirstAvailable(["dc_native.linux-arm64-gnu.node"]);
break;
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`);
}
break;
default:
throw new Error(`Unsupported platform: ${platform}, architecture: ${arch}`);
}
if (!nativeBinding) {
if (loadError) {
throw loadError;
}
throw new Error(`Failed to load dc-native binding`);
}
module.exports = nativeBinding;

15
native/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "dc-native",
"version": "1.0.0",
"description": "Native keychain bindings for Bitwarden Directory Connector",
"main": "index.js",
"types": "index.d.ts",
"license": "GPL-3.0",
"scripts": {
"build": "napi build --platform",
"build:release": "napi build --platform --release"
},
"devDependencies": {
"@napi-rs/cli": "^3.0.0"
}
}

70
native/src/lib.rs Normal file
View File

@@ -0,0 +1,70 @@
#[macro_use]
extern crate napi_derive;
#[napi]
pub mod passwords {
/// The error message returned when a password is not found during retrieval or deletion.
#[napi]
pub const PASSWORD_NOT_FOUND: &str = desktop_core::password::PASSWORD_NOT_FOUND;
/// Fetch the stored password from the keychain.
/// Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
#[napi]
pub async fn get_password(service: String, account: String) -> napi::Result<String> {
desktop_core::password::get_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Save the password to the keychain. Adds an entry if none exists, otherwise updates it.
#[napi]
pub async fn set_password(
service: String,
account: String,
password: String,
) -> napi::Result<()> {
desktop_core::password::set_password(&service, &account, &password)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Delete the stored password from the keychain.
/// Throws an Error with message PASSWORD_NOT_FOUND if the password does not exist.
#[napi]
pub async fn delete_password(service: String, account: String) -> napi::Result<()> {
desktop_core::password::delete_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Check if OS secure storage is available.
#[napi]
pub async fn is_available() -> napi::Result<bool> {
desktop_core::password::is_available()
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Migrate a credential that was stored by keytar (UTF-8 blob) to the new UTF-16 format
/// used by desktop_core on Windows. No-ops on non-Windows platforms.
///
/// Returns true if a migration was performed, false if the credential was already in the
/// correct format or does not exist.
#[napi]
pub async fn migrate_keytar_password(service: String, account: String) -> napi::Result<bool> {
#[cfg(windows)]
{
crate::migration::migrate_keytar_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[cfg(not(windows))]
{
let _ = (service, account);
Ok(false)
}
}
}
#[cfg(windows)]
mod migration;

67
native/src/migration.rs Normal file
View File

@@ -0,0 +1,67 @@
/// Windows-only: migrates credentials stored by keytar (UTF-8 blob via CredWriteA) to the
/// UTF-16 format expected by desktop_core (CredWriteW).
///
/// Keytar used CredWriteA on Windows, which stored the credential blob as raw UTF-8 bytes.
/// desktop_core uses CredWriteW with a UTF-16 encoded blob. Reading old keytar credentials
/// through desktop_core's get_password produces garbled output because the UTF-8 bytes are
/// reinterpreted as UTF-16.
///
/// This function detects the old format by checking whether the raw blob bytes are valid UTF-8
/// without null bytes (UTF-16 LE encoding of ASCII always contains null bytes). If so, it
/// re-saves the credential using desktop_core's set_password (UTF-16 encoding).
use anyhow::{anyhow, Result};
use widestring::U16CString;
use windows::{
core::PCWSTR,
Win32::Security::Credentials::{CredFree, CredReadW, CRED_TYPE_GENERIC},
};
pub async fn migrate_keytar_password(service: &str, account: &str) -> Result<bool> {
let target = format!("{}/{}", service, account);
let target_wide = U16CString::from_str(&target)?;
let mut credential = std::ptr::null_mut();
let result = unsafe {
CredReadW(
PCWSTR(target_wide.as_ptr()),
CRED_TYPE_GENERIC,
None,
&mut credential,
)
};
scopeguard::defer! {{
unsafe { CredFree(credential as *mut _) };
}};
if result.is_err() {
// Credential does not exist; nothing to migrate.
return Ok(false);
}
let blob_bytes: Vec<u8> = unsafe {
let blob_ptr = (*credential).CredentialBlob;
let blob_size = (*credential).CredentialBlobSize as usize;
if blob_ptr.is_null() || blob_size == 0 {
return Ok(false);
}
std::slice::from_raw_parts(blob_ptr, blob_size).to_vec()
};
// UTF-16 LE encoding of ASCII always contains null bytes (e.g. 'A' → 0x41 0x00).
// Keytar stored raw UTF-8 bytes which will never contain null bytes for valid JSON.
// If the blob is valid UTF-8 and contains no null bytes, it was written by keytar.
let blob_is_utf8 = std::str::from_utf8(&blob_bytes)
.map(|s| !s.contains('\0'))
.unwrap_or(false);
if !blob_is_utf8 {
// Already UTF-16 or unrecognised format; no migration needed.
return Ok(false);
}
let utf8_value = String::from_utf8(blob_bytes).map_err(|e| anyhow!(e))?;
desktop_core::password::set_password(service, account, &utf8_value).await?;
Ok(true)
}

13660
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2025.11.0",
"version": "2026.2.0",
"keywords": [
"bitwarden",
"password",
@@ -26,19 +26,20 @@
"symlink:win": "rm -rf ./jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"rebuild": "electron-rebuild",
"reset": "rimraf --glob ./node_modules/keytar/* && npm install",
"build:native": "cd native && npm install && npm run build",
"build:native:release": "cd native && npm install && npm run build:release",
"rebuild": "npm run build:native:release",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"build": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\"",
"build:main": "webpack --config webpack.main.js",
"build:renderer": "webpack --config webpack.renderer.js",
"build:renderer:watch": "webpack --config webpack.renderer.js --watch",
"build:dist": "npm run reset && npm run rebuild && npm run build",
"build:cli": "webpack --config webpack.cli.js",
"build:cli:watch": "webpack --config webpack.cli.js --watch",
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.js",
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.js --watch",
"build:main": "webpack --config webpack.main.cjs",
"build:renderer": "webpack --config webpack.renderer.cjs",
"build:renderer:watch": "webpack --config webpack.renderer.cjs --watch",
"build:dist": "npm run rebuild && npm run build",
"build:cli": "webpack --config webpack.cli.cjs",
"build:cli:watch": "webpack --config webpack.cli.cjs --watch",
"build:cli:prod": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs",
"build:cli:prod:watch": "cross-env NODE_ENV=production webpack --config webpack.cli.cjs --watch",
"electron": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 ./build --watch\" \"npm run build:renderer:watch\"",
"electron:ignore": "npm run build:main && concurrently -k -n Main,Rend -c yellow,cyan \"electron --inspect=5858 --ignore-certificate-errors ./build --watch\" \"npm run build:renderer:watch\"",
"clean:dist": "rimraf --glob ./dist/*",
@@ -73,85 +74,84 @@
"test:types": "npx tsc --noEmit"
},
"devDependencies": {
"@angular-devkit/build-angular": "20.3.3",
"@angular-eslint/eslint-plugin-template": "20.4.0",
"@angular-eslint/template-parser": "20.4.0",
"@angular/compiler-cli": "20.3.3",
"@angular-eslint/eslint-plugin-template": "21.1.0",
"@angular-eslint/template-parser": "21.1.0",
"@angular/build": "21.1.2",
"@angular/compiler-cli": "21.1.1",
"@electron/notarize": "2.5.0",
"@electron/rebuild": "4.0.1",
"@fluffy-spoon/substitute": "1.208.0",
"@microsoft/microsoft-graph-types": "2.43.1",
"@ngtools/webpack": "20.3.3",
"@ngtools/webpack": "21.1.2",
"@types/inquirer": "8.2.10",
"@types/jest": "29.5.14",
"@types/jest": "30.0.0",
"@types/lowdb": "1.0.15",
"@types/node": "22.18.1",
"@types/node": "22.19.2",
"@types/node-fetch": "2.6.12",
"@types/node-forge": "1.3.11",
"@types/proper-lockfile": "4.1.4",
"@types/semver": "7.7.1",
"@types/tldjs": "2.3.4",
"@typescript-eslint/eslint-plugin": "8.46.0",
"@typescript-eslint/parser": "8.46.0",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@yao-pkg/pkg": "5.16.1",
"clean-webpack-plugin": "4.0.0",
"babel-loader": "10.0.0",
"jest-environment-jsdom": "30.2.0",
"concurrently": "9.2.0",
"copy-webpack-plugin": "13.0.0",
"cross-env": "7.0.3",
"css-loader": "7.1.2",
"dotenv": "17.2.0",
"electron": "39.1.0",
"electron": "39.2.1",
"electron-builder": "24.13.3",
"electron-log": "5.4.1",
"electron-reload": "2.0.0-alpha.1",
"electron-store": "8.2.0",
"electron-updater": "6.6.2",
"eslint": "8.57.1",
"electron-updater": "6.7.3",
"eslint": "9.39.1",
"eslint-config-prettier": "10.1.5",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1",
"eslint-plugin-rxjs-angular-x": "0.1.0",
"eslint-plugin-rxjs-x": "0.9.1",
"form-data": "4.0.4",
"glob": "11.1.0",
"glob": "13.0.6",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.3",
"husky": "9.1.7",
"jest": "29.7.0",
"jest": "30.2.0",
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "14.6.0",
"jest-mock-extended": "4.0.0",
"jest-preset-angular": "16.0.0",
"lint-staged": "16.2.6",
"mini-css-extract-plugin": "2.9.2",
"minimatch": "5.1.2",
"node-forge": "1.3.1",
"mini-css-extract-plugin": "2.10.0",
"node-forge": "1.3.2",
"node-loader": "2.1.0",
"prettier": "3.6.2",
"rimraf": "6.0.1",
"prettier": "3.8.1",
"rimraf": "6.1.0",
"rxjs": "7.8.2",
"sass": "1.93.2",
"sass": "1.97.1",
"sass-loader": "16.0.5",
"ts-jest": "29.4.1",
"ts-loader": "9.5.2",
"tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "5.0.1",
"typescript": "5.8.3",
"webpack": "5.102.1",
"type-fest": "5.4.2",
"typescript": "5.9.3",
"webpack": "5.105.1",
"webpack-cli": "6.0.1",
"webpack-merge": "6.0.1",
"webpack-node-externals": "3.0.0",
"zone.js": "0.15.1"
"zone.js": "0.16.0"
},
"dependencies": {
"@angular/animations": "20.3.3",
"@angular/cdk": "20.2.7",
"@angular/cli": "20.3.3",
"@angular/common": "20.3.3",
"@angular/compiler": "20.3.3",
"@angular/core": "20.3.3",
"@angular/forms": "20.3.3",
"@angular/platform-browser": "20.3.3",
"@angular/platform-browser-dynamic": "20.3.3",
"@angular/router": "20.3.3",
"@angular/animations": "21.1.1",
"@angular/cdk": "21.1.1",
"@angular/cli": "21.1.2",
"@angular/common": "21.1.1",
"@angular/compiler": "21.1.1",
"@angular/core": "21.1.1",
"@angular/forms": "21.1.1",
"@angular/platform-browser": "21.1.1",
"@angular/platform-browser-dynamic": "21.1.1",
"@angular/router": "21.1.1",
"@microsoft/microsoft-graph-client": "3.0.7",
"big-integer": "1.6.52",
"bootstrap": "5.3.7",
@@ -162,20 +162,20 @@
"googleapis": "149.0.0",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
"keytar": "7.9.0",
"ldapts": "8.0.1",
"dc-native": "file:./native",
"ldapts": "8.1.3",
"lowdb": "1.0.0",
"ngx-toastr": "19.1.0",
"ngx-toastr": "20.0.4",
"node-fetch": "2.7.0",
"parse5": "8.0.0",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.2",
"tldjs": "2.3.1",
"uuid": "11.1.0",
"zone.js": "0.15.1"
"zone.js": "0.16.0"
},
"engines": {
"node": "~20",
"node": "~22",
"npm": "~10"
},
"lint-staged": {

View File

@@ -23,7 +23,7 @@ import { EnvironmentComponent } from "./environment.component";
// The only subscription in this component is closed from a child component, confusing eslint.
// https://github.com/cartant/eslint-plugin-rxjs-angular/blob/main/docs/rules/prefer-takeuntil.md
//
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
// eslint-disable-next-line rxjs-angular-x/prefer-takeuntil
export class ApiKeyComponent {
@ViewChild("environment", { read: ViewContainerRef, static: true })
environmentModal: ViewContainerRef;
@@ -100,7 +100,7 @@ export class ApiKeyComponent {
this.environmentModal,
);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
// eslint-disable-next-line rxjs-angular-x/prefer-takeuntil
childComponent.onSaved.pipe(takeUntil(modalRef.onClosed)).subscribe(() => {
modalRef.close();
});

View File

@@ -1,10 +1,9 @@
import { enableProdMode } from "@angular/core";
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { isDev } from "@/jslib/electron/src/utils";
// tslint:disable-next-line
require("../scss/styles.scss");
import "../scss/styles.scss";
import { AppModule } from "./app.module";
@@ -12,4 +11,7 @@ if (!isDev()) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
platformBrowserDynamic().bootstrapModule(AppModule, {
applicationProviders: [provideZoneChangeDetection()],
preserveWhitespaces: true,
});

View File

@@ -3,17 +3,25 @@
<div class="card-body">
<p>
{{ "lastGroupSync" | i18n }}:
<span *ngIf="!lastGroupSync">-</span>
@if (!lastGroupSync) {
<span>-</span>
}
{{ lastGroupSync | date: "medium" }}
<br />
{{ "lastUserSync" | i18n }}:
<span *ngIf="!lastUserSync">-</span>
@if (!lastUserSync) {
<span>-</span>
}
{{ lastUserSync | date: "medium" }}
</p>
<p>
{{ "syncStatus" | i18n }}:
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
@if (syncRunning) {
<strong class="text-success">{{ "running" | i18n }}</strong>
}
@if (!syncRunning) {
<strong class="text-danger">{{ "stopped" | i18n }}</strong>
}
</p>
<form #startForm [appApiAction]="startPromise" class="d-inline">
<button
@@ -60,57 +68,85 @@
/>
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
</div>
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
@if (!simForm.loading && (simUsers || simGroups)) {
<hr />
<div class="row">
<div class="col-lg">
<h4>{{ "users" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
{{ "noUsers" | i18n }}
</p>
@if (simEnabledUsers && simEnabledUsers.length) {
<ul class="bwi-ul testing-list">
@for (u of simEnabledUsers; track u) {
<li title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
}
</ul>
}
@if (!simEnabledUsers || !simEnabledUsers.length) {
<p>
{{ "noUsers" | i18n }}
</p>
}
<h4>{{ "disabledUsers" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
{{ "noUsers" | i18n }}
</p>
@if (simDisabledUsers && simDisabledUsers.length) {
<ul class="bwi-ul testing-list">
@for (u of simDisabledUsers; track u) {
<li title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
}
</ul>
}
@if (!simDisabledUsers || !simDisabledUsers.length) {
<p>
{{ "noUsers" | i18n }}
</p>
}
<h4>{{ "deletedUsers" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
{{ "noUsers" | i18n }}
</p>
@if (simDeletedUsers && simDeletedUsers.length) {
<ul class="bwi-ul testing-list">
@for (u of simDeletedUsers; track u) {
<li title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
}
</ul>
}
@if (!simDeletedUsers || !simDeletedUsers.length) {
<p>
{{ "noUsers" | i18n }}
</p>
}
</div>
<div class="col-lg">
<h4>{{ "groups" | i18n }}</h4>
<ul class="bwi-ul testing-list" *ngIf="simGroups && simGroups.length">
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
<i class="bwi bwi-li bwi-sitemap"></i>
{{ g.displayName }}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
{{ u.displayName }}
@if (simGroups && simGroups.length) {
<ul class="bwi-ul testing-list">
@for (g of simGroups; track g) {
<li title="{{ g.referenceId }}">
<i class="bwi bwi-li bwi-sitemap"></i>
{{ g.displayName }}
@if (g.users && g.users.length) {
<ul class="small">
@for (u of g.users; track u) {
<li title="{{ u.referenceId }}">
{{ u.displayName }}
</li>
}
</ul>
}
</li>
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
}
</ul>
}
@if (!simGroups || !simGroups.length) {
<p>{{ "noGroups" | i18n }}</p>
}
</div>
</div>
</ng-container>
}
</div>
</div>

View File

@@ -6,9 +6,11 @@
<div class="mb-3">
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
{{ o.name }}
</option>
@for (o of directoryOptions; track o) {
<option [ngValue]="o.value">
{{ o.name }}
</option>
}
</select>
</div>
<div [hidden]="directory != directoryType.Ldap">
@@ -51,20 +53,22 @@
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
</div>
</div>
<div class="mb-3" *ngIf="!ldap.ad">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="pagedSearch"
[(ngModel)]="ldap.pagedSearch"
name="PagedSearch"
/>
<label class="form-check-label" for="pagedSearch">{{
"ldapPagedResults" | i18n
}}</label>
@if (!ldap.ad) {
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="pagedSearch"
[(ngModel)]="ldap.pagedSearch"
name="PagedSearch"
/>
<label class="form-check-label" for="pagedSearch">{{
"ldapPagedResults" | i18n
}}</label>
</div>
</div>
</div>
}
<div class="mb-3">
<div class="form-check">
<input
@@ -79,116 +83,122 @@
}}</label>
</div>
</div>
<div class="ms-4" *ngIf="ldap.ssl">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="false"
id="ssl"
[(ngModel)]="ldap.startTls"
name="SSL"
/>
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
@if (ldap.ssl) {
<div class="ms-4">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="false"
id="ssl"
[(ngModel)]="ldap.startTls"
name="SSL"
/>
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="true"
id="startTls"
[(ngModel)]="ldap.startTls"
name="StartTLS"
/>
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
</div>
</div>
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="true"
id="startTls"
[(ngModel)]="ldap.startTls"
name="StartTLS"
/>
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
@if (ldap.startTls) {
<div class="ms-4">
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')"
/>
<input
type="text"
class="form-control"
id="tlsCaPath"
name="TLSCaPath"
[(ngModel)]="ldap.tlsCaPath"
/>
</div>
</div>
}
@if (!ldap.startTls) {
<div class="ms-4">
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCertPath_file"
(change)="setSslPath('sslCertPath')"
/>
<input
type="text"
class="form-control"
id="sslCertPath"
name="SSLCertPath"
[(ngModel)]="ldap.sslCertPath"
/>
</div>
<div class="mb-3">
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslKeyPath_file"
(change)="setSslPath('sslKeyPath')"
/>
<input
type="text"
class="form-control"
id="sslKeyPath"
name="SSLKeyPath"
[(ngModel)]="ldap.sslKeyPath"
/>
</div>
<div class="mb-3">
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCaPath_file"
(change)="setSslPath('sslCaPath')"
/>
<input
type="text"
class="form-control"
id="sslCaPath"
name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath"
/>
</div>
</div>
}
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized"
name="CertDoNoVerify"
/>
<label class="form-check-label" for="certDoNotVerify">{{
"ldapCertDoNotVerify" | i18n
}}</label>
</div>
</div>
</div>
<div class="ms-4" *ngIf="ldap.startTls">
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')"
/>
<input
type="text"
class="form-control"
id="tlsCaPath"
name="TLSCaPath"
[(ngModel)]="ldap.tlsCaPath"
/>
</div>
</div>
<div class="ms-4" *ngIf="!ldap.startTls">
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCertPath_file"
(change)="setSslPath('sslCertPath')"
/>
<input
type="text"
class="form-control"
id="sslCertPath"
name="SSLCertPath"
[(ngModel)]="ldap.sslCertPath"
/>
</div>
<div class="mb-3">
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslKeyPath_file"
(change)="setSslPath('sslKeyPath')"
/>
<input
type="text"
class="form-control"
id="sslKeyPath"
name="SSLKeyPath"
[(ngModel)]="ldap.sslKeyPath"
/>
</div>
<div class="mb-3">
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCaPath_file"
(change)="setSslPath('sslCaPath')"
/>
<input
type="text"
class="form-control"
id="sslCaPath"
name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath"
/>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized"
name="CertDoNoVerify"
/>
<label class="form-check-label" for="certDoNotVerify">{{
"ldapCertDoNotVerify" | i18n
}}</label>
</div>
</div>
</div>
}
<div class="mb-3" [hidden]="true">
<div class="form-check">
<input
@@ -211,10 +221,12 @@
name="Username"
[(ngModel)]="ldap.username"
/>
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} company\admin</div>
<div class="form-text" *ngIf="!ldap.ad">
{{ "ex" | i18n }} cn=admin,dc=company,dc=com
</div>
@if (ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} company\admin</div>
}
@if (!ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} cn=admin,dc=company,dc=com</div>
}
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ "password" | i18n }}</label>
@@ -604,18 +616,24 @@
name="UserFilter"
[(ngModel)]="sync.userFilter"
></textarea>
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))
</div>
<div class="form-text" *ngIf="directory === directoryType.EntraID">
{{ "ex" | i18n }} exclude:joe&#64;company.com
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgUnitPath=/Engineering
</div>
@if (directory === directoryType.Ldap) {
<div class="form-text">
{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))
</div>
}
@if (directory === directoryType.EntraID) {
<div class="form-text">{{ "ex" | i18n }} exclude:joe&#64;company.com</div>
}
@if (directory === directoryType.Okta) {
<div class="form-text">
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div>
}
@if (directory === directoryType.GSuite) {
<div class="form-text">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgUnitPath=/Engineering
</div>
}
</div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
@@ -681,18 +699,20 @@
name="GroupFilter"
[(ngModel)]="sync.groupFilter"
></textarea>
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
</div>
<div class="form-text" *ngIf="directory === directoryType.EntraID">
{{ "ex" | i18n }} include:Sales,IT
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} include:Sales,IT
</div>
@if (directory === directoryType.Ldap) {
<div class="form-text">
{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
</div>
}
@if (directory === directoryType.EntraID) {
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
}
@if (directory === directoryType.Okta) {
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</div>
}
@if (directory === directoryType.GSuite) {
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
}
</div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
@@ -703,8 +723,12 @@
name="GroupPath"
[(ngModel)]="sync.groupPath"
/>
<div class="form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</div>
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</div>
@if (!ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} CN=Groups</div>
}
@if (ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} CN=Users</div>
}
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="mb-3">

View File

@@ -24,8 +24,8 @@ import { AuthService } from "./services/auth.service";
import { BatchRequestBuilder } from "./services/batch-request-builder";
import { DefaultDirectoryFactoryService } from "./services/directory-factory.service";
import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NativeSecureStorageService } from "./services/nativeSecureStorage.service";
import { SingleRequestBuilder } from "./services/single-request-builder";
import { StateService } from "./services/state.service";
import { StateMigrationService } from "./services/stateMigration.service";
@@ -100,7 +100,7 @@ export class Main {
);
this.secureStorageService = plaintextSecrets
? this.storageService
: new KeytarSecureStorageService(applicationName);
: new NativeSecureStorageService(applicationName);
this.stateMigrationService = new StateMigrationService(
this.storageService,

View File

@@ -1,11 +1,11 @@
import { passwords } from "dc-native";
import { ipcMain } from "electron";
import { deletePassword, getPassword, setPassword } from "keytar";
export class DCCredentialStorageListener {
constructor(private serviceName: string) {}
init() {
ipcMain.on("keytar", async (event: any, message: any) => {
ipcMain.on("nativeSecureStorage", async (event: any, message: any) => {
try {
let serviceName = this.serviceName;
message.keySuffix = "_" + (message.keySuffix ?? "");
@@ -16,14 +16,14 @@ export class DCCredentialStorageListener {
let val: string | boolean = null;
if (message.action && message.key) {
if (message.action === "getPassword") {
val = await getPassword(serviceName, message.key);
val = await passwords.getPassword(serviceName, message.key);
} else if (message.action === "hasPassword") {
const result = await getPassword(serviceName, message.key);
const result = await passwords.getPassword(serviceName, message.key);
val = result != null;
} else if (message.action === "setPassword" && message.value) {
await setPassword(serviceName, message.key, message.value);
await passwords.setPassword(serviceName, message.key, message.value);
} else if (message.action === "deletePassword") {
await deletePassword(serviceName, message.key);
await passwords.deletePassword(serviceName, message.key);
}
}
event.returnValue = val;

View File

@@ -9,7 +9,7 @@ import { MenuMain } from "./menu.main";
const SyncCheckInterval = 60 * 1000; // 1 minute
export class MessagingMain {
private syncTimeout: NodeJS.Timeout;
private syncTimeout: ReturnType<typeof setTimeout>;
constructor(
private windowMain: WindowMain,

View File

@@ -28,4 +28,4 @@ $danger: map_get($theme-colors, "danger");
$secondary: map_get($theme-colors, "secondary");
$secondary-alt: map_get($theme-colors, "secondary-alt");
@import "~bootstrap/scss/bootstrap.scss";
@import "bootstrap/scss/bootstrap.scss";

View File

@@ -1,4 +1,4 @@
@import "~bootstrap/scss/_variables.scss";
@import "bootstrap/scss/_variables.scss";
html.os_windows {
body {

View File

@@ -1,4 +1,4 @@
@import "~bootstrap/scss/_variables.scss";
@import "bootstrap/scss/_variables.scss";
body {
padding: 10px 0 20px 0;

View File

@@ -1,6 +1,6 @@
@import "~ngx-toastr/toastr";
@import "ngx-toastr/toastr";
@import "~bootstrap/scss/_variables.scss";
@import "bootstrap/scss/_variables.scss";
.toast-container {
.toast-close-button {

View File

@@ -1,7 +1,8 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { mock } from "jest-mock-extended";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import {
@@ -11,7 +12,6 @@ import {
} from "@/jslib/common/src/models/domain/account";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { MessagingService } from "../../jslib/common/src/abstractions/messaging.service";
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
import { AuthService } from "./auth.service";
@@ -35,22 +35,22 @@ export function identityTokenResponseFactory() {
}
describe("AuthService", () => {
let apiService: SubstituteOf<ApiService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let stateService: SubstituteOf<StateService>;
let apiService: jest.Mocked<ApiService>;
let appIdService: jest.Mocked<AppIdService>;
let platformUtilsService: jest.Mocked<PlatformUtilsService>;
let messagingService: jest.Mocked<MessagingService>;
let stateService: jest.Mocked<StateService>;
let authService: AuthService;
beforeEach(async () => {
apiService = Substitute.for();
appIdService = Substitute.for();
platformUtilsService = Substitute.for();
stateService = Substitute.for();
messagingService = Substitute.for();
apiService = mock<ApiService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
stateService = mock<StateService>();
messagingService = mock<MessagingService>();
appIdService.getAppId().resolves(deviceId);
appIdService.getAppId.mockResolvedValue(deviceId);
authService = new AuthService(
apiService,
@@ -62,11 +62,12 @@ describe("AuthService", () => {
});
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
await authService.logIn({ clientId, clientSecret });
stateService.received(1).addAccount(
expect(stateService.addAccount).toHaveBeenCalledTimes(1);
expect(stateService.addAccount).toHaveBeenCalledWith(
new Account({
profile: {
...new AccountProfile(),

View File

@@ -132,7 +132,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
}
const setFilter = this.createCustomUserSet(this.syncConfig.userFilter);
// eslint-disable-next-line
while (true) {
const users: graphType.User[] = res.value;
if (users != null) {
@@ -211,7 +211,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
let auMembers = await this.client
.api(`${this.getGraphApiEndpoint()}/v1.0/directory/administrativeUnits/${p}/members`)
.get();
// eslint-disable-next-line
while (true) {
for (const auMember of auMembers.value) {
const groupId = auMember.id;
@@ -328,7 +328,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
const entries: GroupEntry[] = [];
const groupsReq = this.client.api("/groups");
let res = await groupsReq.get();
// eslint-disable-next-line
while (true) {
const groups: graphType.Group[] = res.value;
if (groups != null) {
@@ -421,7 +421,7 @@ export class EntraIdDirectoryService extends BaseDirectoryService implements IDi
const memReq = this.client.api("/groups/" + group.id + "/members");
let memRes = await memReq.get();
// eslint-disable-next-line
while (true) {
const members: any = memRes.value;
if (members != null) {

View File

@@ -71,7 +71,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
let nextPageToken: string = null;
const filter = this.createCustomSet(this.syncConfig.userFilter);
// eslint-disable-next-line
while (true) {
this.logService.info("Querying users - nextPageToken:" + nextPageToken);
const p = Object.assign({ query: query, pageToken: nextPageToken }, this.authParams);
@@ -99,7 +99,7 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
}
nextPageToken = null;
// eslint-disable-next-line
while (true) {
this.logService.info("Querying deleted users - nextPageToken:" + nextPageToken);
const p = Object.assign(
@@ -154,7 +154,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
const query = this.createDirectoryQuery(this.syncConfig.groupFilter);
let nextPageToken: string = null;
// eslint-disable-next-line
while (true) {
this.logService.info("Querying groups - nextPageToken:" + nextPageToken);
let p = null;
@@ -194,7 +193,6 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
entry.externalId = group.id;
entry.name = group.name;
// eslint-disable-next-line
while (true) {
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
const memRes = await this.service.members.list(p);

View File

@@ -68,10 +68,12 @@ export class LdapDirectoryService implements IDirectoryService {
}
groups = await this.getGroups(groupForce);
}
} finally {
} catch (e) {
await this.client.unbind();
throw e;
}
await this.client.unbind();
return [groups, users];
}
@@ -453,8 +455,9 @@ export class LdapDirectoryService implements IDirectoryService {
try {
await this.client.bind(user, pass);
} catch {
} catch (error) {
await this.client.unbind();
throw error;
}
}

View File

@@ -1,31 +0,0 @@
import { deletePassword, getPassword, setPassword } from "keytar";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
export class KeytarSecureStorageService implements StorageService {
constructor(private serviceName: string) {}
get<T>(key: string): Promise<T> {
return getPassword(this.serviceName, key).then((val) => {
return JSON.parse(val) as T;
});
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
save(key: string, obj: any): Promise<any> {
// keytar throws if you try to save a falsy value: https://github.com/atom/node-keytar/issues/86
// handle this by removing the key instead
if (!obj) {
return this.remove(key);
}
return setPassword(this.serviceName, key, JSON.stringify(obj));
}
remove(key: string): Promise<any> {
return deletePassword(this.serviceName, key);
}
}

View File

@@ -0,0 +1,28 @@
import { passwords } from "dc-native";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
export class NativeSecureStorageService implements StorageService {
constructor(private serviceName: string) {}
get<T>(key: string): Promise<T> {
return passwords.getPassword(this.serviceName, key).then((val) => {
return JSON.parse(val) as T;
});
}
async has(key: string): Promise<boolean> {
return (await this.get(key)) != null;
}
save(key: string, obj: any): Promise<any> {
if (!obj) {
return this.remove(key);
}
return passwords.setPassword(this.serviceName, key, JSON.stringify(obj));
}
remove(key: string): Promise<any> {
return passwords.deletePassword(this.serviceName, key);
}
}

View File

@@ -1,3 +1,5 @@
import { passwords } from "dc-native";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StateMigrationService as BaseStateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
@@ -61,6 +63,13 @@ export class StateMigrationService extends BaseStateMigrationService {
break;
case StateVersion.Two:
await this.migrateStateFrom2To3();
break;
case StateVersion.Three:
await this.migrateStateFrom3To4();
break;
case StateVersion.Four:
await this.migrateStateFrom4To5();
break;
}
currentStateVersion += 1;
}
@@ -168,6 +177,50 @@ export class StateMigrationService extends BaseStateMigrationService {
}
}
}
/**
* Migrates Windows credential store entries previously written by keytar (UTF-8 blob) to
* the UTF-16 format expected by desktop_core. No-ops on non-Windows platforms.
*
* This migration is needed because keytar used CredWriteA (storing blobs as raw UTF-8 bytes)
* while desktop_core uses CredWriteW (storing blobs as UTF-16). Reading old keytar credentials
* through desktop_core produces garbled output without this migration.
*/
protected async migrateStateFrom3To4(useSecureStorageForSecrets = true): Promise<void> {
if (useSecureStorageForSecrets && process.platform === "win32") {
const serviceName = "Bitwarden Directory Connector";
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);
if (authenticatedUserIds?.length) {
const credentialKeys = [
SecureStorageKeys.ldap,
SecureStorageKeys.gsuite,
SecureStorageKeys.azure,
SecureStorageKeys.entra,
SecureStorageKeys.okta,
SecureStorageKeys.oneLogin,
];
await Promise.all(
authenticatedUserIds.flatMap((userId) =>
credentialKeys.map((key) =>
passwords.migrateKeytarPassword(serviceName, `${userId}_${key}`),
),
),
);
}
}
const globals = await this.getGlobals();
globals.stateVersion = StateVersion.Four;
await this.set(StateKeys.global, globals);
}
protected async migrateStateFrom4To5(): Promise<void> {
const globals = await this.getGlobals();
globals.stateVersion = StateVersion.Five;
await this.set(StateKeys.global, globals);
}
protected async migrateStateFrom2To3(useSecureStorageForSecrets = true): Promise<void> {
if (useSecureStorageForSecrets) {
const authenticatedUserIds = await this.get<string[]>(StateKeys.authenticatedAccounts);

View File

@@ -116,6 +116,7 @@ describe("SyncService", () => {
stateService.getLastSyncHash.mockResolvedValue("unique hash");
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
// eslint-disable-next-line no-import-assign
constants.batchSize = 4;
const syncResult = await syncService.sync(false, false);
@@ -130,6 +131,7 @@ describe("SyncService", () => {
expect(apiService.postPublicImportDirectory).toHaveBeenCalledTimes(7);
// @ts-expect-error Reset batch size to original state.
// eslint-disable-next-line no-import-assign
constants.batchSize = originalBatchSize;
});
});

View File

@@ -97,6 +97,7 @@ describe("SyncService", () => {
stateService.getLastSyncHash.mockResolvedValue("unique hash");
// @ts-expect-error This is a workaround to make the batchsize smaller to trigger the batching logic since its a const.
// eslint-disable-next-line no-import-assign
constants.batchSize = 4;
const mockRequests = new Array(6).fill({
@@ -119,6 +120,7 @@ describe("SyncService", () => {
expect(apiService.postPublicImportDirectory).toHaveBeenCalledWith(mockRequests[5]);
// @ts-expect-error Reset batch size back to original value.
// eslint-disable-next-line no-import-assign
constants.batchSize = originalBatchSize;
});

View File

@@ -1,7 +1,7 @@
import { webcrypto } from "crypto";
import { TextEncoder, TextDecoder } from "util";
import "jest-preset-angular/setup-jest";
Object.assign(globalThis, { TextEncoder, TextDecoder });
Object.defineProperty(window, "CSS", { value: null });
Object.defineProperty(window, "getComputedStyle", {
value: () => {

View File

@@ -5,7 +5,7 @@
},
"compilerOptions": {
"pretty": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"noImplicitAny": true,
"target": "ES2016",
"module": "ES2020",

View File

@@ -1,6 +1,5 @@
const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const webpack = require("webpack");
@@ -24,7 +23,6 @@ const moduleRules = [
];
const plugins = [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{ from: "./src/locales", to: "locales" }],
}),
@@ -64,6 +62,7 @@ const config = {
output: {
filename: "[name].js",
path: path.resolve(__dirname, "build-cli"),
clean: true,
},
module: { rules: moduleRules },
plugins: plugins,

View File

@@ -1,7 +1,6 @@
const path = require("path");
const { merge } = require("webpack-merge");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const nodeExternals = require("webpack-node-externals");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
@@ -23,6 +22,7 @@ const common = {
output: {
filename: "[name].js",
path: path.resolve(__dirname, "build"),
clean: true,
},
};
@@ -48,7 +48,6 @@ const main = {
],
},
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
"./package.json",
@@ -59,7 +58,7 @@ const main = {
],
externals: {
"electron-reload": "commonjs2 electron-reload",
keytar: "commonjs2 keytar",
"dc-native": "commonjs2 dc-native",
},
};

View File

@@ -55,6 +55,9 @@ const renderer = {
node: {
__dirname: false,
},
externals: {
"dc-native": "commonjs2 dc-native",
},
entry: {
"app/main": "./src/app/main.ts",
},