1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-01-14 06:23:52 +00:00

Compare commits

..

22 Commits

Author SHA1 Message Date
Brandon
5a68f7af48 add missing polyfils for TextEncoder & TextDecoder 2026-01-13 16:07:30 -05:00
Brandon
98484a5762 updated jest, add babel & jest-enveironment-jsdom 2026-01-13 15:13:08 -05:00
Brandon
d104a12de3 @ngtools-webpack@21 and jest-preset-angular@16 2026-01-13 15:13:01 -05:00
Brandon
b30f001239 NG 21 WIP 2026-01-13 15:12:27 -05:00
Brandon
c9531a34af update @angular/cdk@21 2026-01-13 15:12:19 -05:00
Brandon
45bc60527f ng update 21 wip 2026-01-13 15:12:12 -05:00
Brandon
d98d9c9036 update jest to v.30.2.0 2026-01-13 15:12:02 -05: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
52 changed files with 4920 additions and 8041 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"
}
}
]
}

View File

@@ -56,7 +56,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -111,7 +111,7 @@ jobs:
fi
- name: Upload Linux Zip to GitHub
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-linux-${{ env._PACKAGE_VERSION }}.zip
@@ -134,7 +134,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -182,7 +182,7 @@ jobs:
fi
- name: Upload Mac Zip to GitHub
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-macos-${{ env._PACKAGE_VERSION }}.zip
@@ -209,7 +209,7 @@ jobs:
choco install checksum --no-progress
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -258,7 +258,7 @@ jobs:
}
- name: Upload Windows Zip to GitHub
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
path: ./dist-cli/bwdc-windows-${{ env._PACKAGE_VERSION }}.zip
@@ -284,7 +284,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -338,28 +338,28 @@ jobs:
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
- name: Upload Portable Executable to GitHub
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable to GitHub
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload Installer Executable Blockmap to GitHub
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
path: ./dist/Bitwarden-Connector-Installer-${{ env._PACKAGE_VERSION }}.exe.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest.yml
path: ./dist/latest.yml
@@ -384,7 +384,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -411,14 +411,14 @@ jobs:
run: npm run dist:lin
- name: Upload AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest-linux.yml
path: ./dist/latest-linux.yml
@@ -444,7 +444,7 @@ jobs:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -542,28 +542,28 @@ jobs:
CSC_FOR_PULL_REQUEST: true
- name: Upload .zip artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg
if-no-files-found: error
- name: Upload .dmg Blockmap artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
path: ./dist/Bitwarden-Connector-${{ env._PACKAGE_VERSION }}.dmg.blockmap
if-no-files-found: error
- name: Upload latest auto-update artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: latest-mac.yml
path: ./dist/latest-mac.yml

View File

@@ -52,7 +52,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -129,7 +129,7 @@ jobs:
- name: Report test results
id: report
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
# PRs from the repository and all other events are OK.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
@@ -140,7 +140,7 @@ 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/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1

View File

@@ -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

@@ -34,7 +34,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -53,7 +53,7 @@ jobs:
run: npm run test --coverage
- name: Report test results
uses: dorny/test-reporter@dc3a92680fcc15842eef52e8c4606ea7ce6bd3f3 # v2.1.1
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
# PRs from the repository and all other events are OK.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
@@ -64,7 +64,7 @@ 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/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1

View File

@@ -42,7 +42,7 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Generate GH App token
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
id: app-token
with:
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}

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,7 +4,7 @@
},
"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",

149
eslint.config.mjs Normal file
View File

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

View File

@@ -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

@@ -13,42 +13,47 @@ import {
@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>
}
`,
animations: [
trigger("flyInOut", [

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

@@ -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

@@ -33,5 +33,5 @@ export function makeStaticByteArray(length: number, start = 0) {
for (let i = 0; i < length; i++) {
arr[i] = start + i;
}
return arr;
return arr.buffer;
}

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

@@ -1,4 +1,4 @@
import * as hrtime from "browser-hrtime";
import hrtime from "browser-hrtime";
import { LogService as LogServiceAbstraction } from "../abstractions/log.service";
import { LogLevelType } from "../enums/logLevelType";

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

@@ -1,11 +1,12 @@
import * as fs from "fs";
import { ipcMain } from "electron";
import Store from "electron-store";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
// eslint-disable-next-line
const Store = require("electron-store");
export class ElectronStorageService implements StorageService {
private store: any;

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) {
@@ -241,7 +240,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,

View File

@@ -1,8 +1,8 @@
import * as fs from "fs";
import * as path from "path";
import * as lowdb from "lowdb";
import * as FileSync from "lowdb/adapters/FileSync";
import Lowdb from "lowdb";
import FileSync from "lowdb/adapters/FileSync";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
@@ -12,7 +12,7 @@ import { Utils } from "@/jslib/common/src/misc/utils";
export class LowdbStorageService implements StorageService {
protected dataFilePath: string;
private db: lowdb.LowdbSync<any>;
private db: Lowdb.LowdbSync<any>;
private defaults: any;
private ready = false;
@@ -32,7 +32,7 @@ export class LowdbStorageService implements StorageService {
}
this.logService.info("Initializing lowdb storage service.");
let adapter: lowdb.AdapterSync<any>;
let adapter: Lowdb.AdapterSync<any>;
if (Utils.isNode && this.dir != null) {
if (!fs.existsSync(this.dir)) {
this.logService.warning(`Could not find dir, "${this.dir}"; creating it instead.`);
@@ -56,7 +56,7 @@ export class LowdbStorageService implements StorageService {
}
try {
this.logService.info("Attempting to create lowdb storage adapter.");
this.db = lowdb(adapter);
this.db = Lowdb(adapter);
this.logService.info("Successfully created lowdb storage adapter.");
} catch (e) {
if (e instanceof SyntaxError) {
@@ -73,7 +73,7 @@ export class LowdbStorageService implements StorageService {
});
}
adapter.write({});
this.db = lowdb(adapter);
this.db = Lowdb(adapter);
} else {
this.logService.error(`Error creating lowdb storage adapter, "${e.message}".`);
throw e;

11843
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.1.0",
"keywords": [
"bitwarden",
"password",
@@ -31,14 +31,14 @@
"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: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 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: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,27 +73,30 @@
"test:types": "npx tsc --noEmit"
},
"devDependencies": {
"@angular-devkit/build-angular": "20.3.3",
"@angular-eslint/eslint-plugin-template": "20.6.0",
"@angular-eslint/template-parser": "20.6.0",
"@angular/compiler-cli": "20.3.15",
"@angular-eslint/eslint-plugin-template": "21.1.0",
"@angular-eslint/template-parser": "21.1.0",
"@angular/build": "21.0.5",
"@angular/compiler-cli": "21.0.8",
"@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.0.5",
"@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.48.0",
"@typescript-eslint/parser": "8.48.0",
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0",
"@yao-pkg/pkg": "5.16.1",
"babel-loader": "9.2.1",
"clean-webpack-plugin": "4.0.0",
"jest-environment-jsdom": "30.2.0",
"concurrently": "9.2.0",
"copy-webpack-plugin": "13.0.0",
"cross-env": "7.0.3",
@@ -103,23 +106,23 @@
"electron-builder": "24.13.3",
"electron-log": "5.4.1",
"electron-reload": "2.0.0-alpha.1",
"electron-store": "11.0.2",
"electron-store": "8.2.0",
"electron-updater": "6.6.2",
"eslint": "8.57.1",
"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.8.3",
"form-data": "4.0.4",
"glob": "13.0.0",
"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": "4.0.0",
"jest-preset-angular": "14.6.0",
"jest-preset-angular": "16.0.0",
"lint-staged": "16.2.6",
"mini-css-extract-plugin": "2.9.2",
"minimatch": "5.1.2",
@@ -128,30 +131,30 @@
"prettier": "3.7.4",
"rimraf": "6.1.0",
"rxjs": "7.8.2",
"sass": "1.94.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.3.0",
"typescript": "5.8.3",
"webpack": "5.103.0",
"typescript": "5.9.3",
"webpack": "5.104.1",
"webpack-cli": "6.0.1",
"webpack-merge": "6.0.1",
"webpack-node-externals": "3.0.0",
"zone.js": "0.15.1"
},
"dependencies": {
"@angular/animations": "20.3.15",
"@angular/cdk": "20.2.14",
"@angular/cli": "20.3.3",
"@angular/common": "20.3.15",
"@angular/compiler": "20.3.15",
"@angular/core": "20.3.15",
"@angular/forms": "20.3.15",
"@angular/platform-browser": "20.3.15",
"@angular/platform-browser-dynamic": "20.3.15",
"@angular/router": "20.3.15",
"@angular/animations": "21.0.8",
"@angular/cdk": "21.0.6",
"@angular/cli": "21.0.5",
"@angular/common": "21.0.8",
"@angular/compiler": "21.0.8",
"@angular/core": "21.0.8",
"@angular/forms": "21.0.8",
"@angular/platform-browser": "21.0.8",
"@angular/platform-browser-dynamic": "21.0.8",
"@angular/router": "21.0.8",
"@microsoft/microsoft-graph-client": "3.0.7",
"big-integer": "1.6.52",
"bootstrap": "5.3.7",

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

@@ -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";
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

@@ -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

@@ -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

@@ -8,10 +8,9 @@
"moduleResolution": "bundler",
"noImplicitAny": true,
"target": "ES2016",
"esModuleInterop": true,
"module": "ES2020",
"lib": ["es5", "es6", "es7", "dom"],
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": false,