diff --git a/.checkmarx/config.yml b/.checkmarx/config.yml index 18b9be6a7e..e45e83fcac 100644 --- a/.checkmarx/config.yml +++ b/.checkmarx/config.yml @@ -7,5 +7,6 @@ checkmarx: scan: configs: sast: + presetName: "BW ASA Premium" # Exclude spec files, and test specific files filter: "!*.spec.ts,!**/spec/**,!apps/desktop/native-messaging-test-runner/**" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bfce3c1547..e9c1f229a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -61,6 +61,7 @@ apps/web/src/app/billing @bitwarden/team-billing-dev libs/angular/src/billing @bitwarden/team-billing-dev libs/common/src/billing @bitwarden/team-billing-dev libs/billing @bitwarden/team-billing-dev +bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev ## Platform team files ## apps/browser/src/platform @bitwarden/team-platform-dev @@ -83,6 +84,7 @@ apps/web/src/translation-constants.ts @bitwarden/team-platform-dev ## Autofill team files ## apps/browser/src/autofill @bitwarden/team-autofill-dev +apps/desktop/src/autofill @bitwarden/team-autofill-dev libs/common/src/autofill @bitwarden/team-autofill-dev ## Component Library ## diff --git a/.github/renovate.json b/.github/renovate.json index bd9ea0da5c..95fd2dc11e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -16,6 +16,10 @@ "matchManagers": ["cargo"], "commitMessagePrefix": "[deps] Platform:" }, + { + "groupName": "napi", + "matchPackageNames": ["napi", "napi-build", "napi-derive"] + }, { "matchPackageNames": ["typescript", "zone.js"], "matchUpdateTypes": ["major", "minor"], diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 585a888ae1..23f4bd35f1 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -160,9 +160,9 @@ jobs: run: npm run dist working-directory: browser-source/apps/browser - # - name: Build Manifest v3 - # run: npm run dist:mv3 - # working-directory: browser-source/apps/browser + - name: Build Manifest v3 + run: npm run dist:mv3 + working-directory: browser-source/apps/browser - name: Gulp run: gulp ci @@ -189,12 +189,12 @@ jobs: path: browser-source/apps/browser/dist/dist-chrome.zip if-no-files-found: error - # - name: Upload Chrome MV3 artifact - # uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - # with: - # name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip - # path: browser-source/apps/browser/dist/dist-chrome-mv3.zip - # if-no-files-found: error + - name: Upload Chrome MV3 artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-chrome-mv3.zip + if-no-files-found: error - name: Upload Firefox artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 2c28d0cb52..e73f882bb4 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -444,7 +444,10 @@ jobs: macos-build: name: MacOS Build - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} @@ -602,7 +605,10 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - browser-build - macos-build @@ -808,7 +814,10 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - browser-build - macos-build @@ -1006,7 +1015,10 @@ jobs: macos-package-dev: name: MacOS Package Dev Release Asset if: false # We need to look into how code signing works for dev - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - browser-build - macos-build diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index abd2538773..8576fb6760 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -299,7 +299,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - name: Trigger web vault deploy + - name: Trigger web vault deploy using GitHub Run ID uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -311,7 +311,7 @@ jobs: ref: 'main', inputs: { 'environment': 'USDEV', - 'branch-or-tag': 'main' + 'build-web-run-id': '${{ github.run_id }}' } }) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 2d784652a5..769e700588 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -27,6 +27,10 @@ on: description: "Debug mode" type: boolean default: true + build-web-run-id: + description: "Build-web workflow Run ID to use for artifact download" + type: string + required: false workflow_call: inputs: @@ -46,6 +50,10 @@ on: description: "Debug mode" type: boolean default: true + build-web-run-id: + description: "Build-web workflow Run ID to use for artifact download" + type: string + required: false permissions: deployments: write @@ -168,7 +176,20 @@ jobs: env: _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} steps: + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' + if: ${{ inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' + if: ${{ !inputs.build-web-run-id }} uses: bitwarden/gh-actions/download-artifacts@main id: download-artifacts continue-on-error: true @@ -249,7 +270,20 @@ jobs: keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant" + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' + if: ${{ inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + - name: 'Download cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' + if: ${{ !inputs.build-web-run-id }} uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-web.yml diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 20bffb956e..b9e2d7a8c8 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -393,7 +393,10 @@ jobs: macos-build: name: MacOS Build - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: setup env: _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} @@ -522,7 +525,10 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - setup - macos-build @@ -732,7 +738,10 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset - runs-on: macos-13 + # Note, this workflow is running on macOS 11 to maintain compatibility with macOS 10.15 Catalina, + # as the newer versions will case the native modules to be incompatible with older macOS systems + # This version should stay pinned until we drop support for macOS 10.15, or we drop the native modules + runs-on: macos-11 needs: - setup - macos-build diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index ea9e69226a..9aa6745faa 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -10,8 +10,6 @@ on: pull_request_target: types: [opened, synchronize] -permissions: read-all - jobs: check-run: name: Check PR run @@ -22,6 +20,8 @@ jobs: runs-on: ubuntu-22.04 needs: check-run permissions: + contents: read + pull-requests: write security-events: write steps: @@ -40,10 +40,13 @@ jobs: base_uri: https://ast.checkmarx.net/ cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }} cx_client_secret: ${{ secrets.CHECKMARX_SECRET }} - additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }} + additional_params: | + --report-format sarif \ + --filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \ + --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 + uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 with: sarif_file: cx_result.sarif @@ -51,6 +54,9 @@ jobs: name: Quality scan runs-on: ubuntu-22.04 needs: check-run + permissions: + contents: read + pull-requests: write steps: - name: Check out repo diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 5aec22926d..246ca9a533 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -367,21 +367,27 @@ jobs: id: set-final-version-output run: | if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then - echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_browser=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then - echo "version=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then - echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_browser=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT + fi + + if [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then + echo "version_cli=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then - echo "version=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then - echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_cli=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT + fi + + if [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then + echo "version_desktop=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then - echo "version=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT - elif [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then - echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT + echo "version_desktop=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT + fi + + if [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then + echo "version_web=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then - echo "version=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT + echo "version_web=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT fi - name: Check if version changed diff --git a/.storybook/main.ts b/.storybook/main.ts index 544beb48c7..c71a74c2a7 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -38,9 +38,7 @@ const config: StorybookConfig = { }, env: (config) => ({ ...config, - FLAGS: JSON.stringify({ - secretsManager: true, - }), + FLAGS: JSON.stringify({}), }), webpackFinal: async (config, { configType }) => { if (config.resolve) { diff --git a/apps/browser/.eslintrc.json b/apps/browser/.eslintrc.json new file mode 100644 index 0000000000..ba96051183 --- /dev/null +++ b/apps/browser/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "webextensions": true + }, + "overrides": [ + { + "files": ["src/**/*.ts"], + "excludedFiles": [ + "src/**/{content,popup,spec}/**/*.ts", + "src/**/autofill/{notification,overlay}/**/*.ts", + "src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts", + "src/**/*.spec.ts" + ], + "rules": { + "no-restricted-globals": [ + "error", + { + "name": "window", + "message": "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead." + } + ] + } + } + ] +} diff --git a/apps/browser/package.json b/apps/browser/package.json index b03469bbb7..ee6d100572 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.3.0", + "version": "2024.4.1", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 5e3a0152a5..1f7c5bbe98 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 5d17d567fc..2111ea6704 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -2706,7 +2706,7 @@ "message": "Hesabınız üçün Duo iki addımlı giriş tələb olunur." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Girişi tamamlamaq üçün uzantını aç." }, "popoutExtension": { "message": "Popout uzantısı" @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Kimlik məlumatlarını saxlama xətası. Detallar üçün konsolu yoxlayın.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Parolu sil" + }, + "passkeyRemoved": { + "message": "Parol silindi" + }, + "unassignedItemsBanner": { + "message": "Bildiriş: Təyin edilməmiş təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünməyəndir və yalnız Admin Konsolu vasitəsilə əlçatandır. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." + }, + "unassignedItemsBannerSelfHost": { + "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünməyən və yalnız Admin Konsolu vasitəsilə əlçatan olacaq. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index f05102d29f..08cb351abb 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index e96a48b3f0..87dfc8d3be 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Грешка при запазването на идентификационните данни. Вижте конзолата за подробности.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Премахване на секретния ключ" + }, + "passkeyRemoved": { + "message": "Секретният ключ е премахнат" + }, + "unassignedItemsBanner": { + "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + }, + "unassignedItemsBannerSelfHost": { + "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index cfaed770c1..1bdaeef7c6 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 9260f5c902..92a667afeb 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index b77edf5611..fc67602b60 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "S'ha produït un error en guardar les credencials. Consulteu la consola per obtenir més informació.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Suprimeix la clau de pas" + }, + "passkeyRemoved": { + "message": "Clau de pas suprimida" + }, + "unassignedItemsBanner": { + "message": "Nota: els elements de l'organització sense assignar ja no es veuran a la vista \"Totes les caixes fortes\" i només es veuran des de la consola d'administració. Assigneu-los-hi una col·lecció des de la consola per fer-los visibles." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index bc2d7e8fd8..d989d25bf2 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Chyba při ukládání přihlašovacích údajů. Podrobnosti naleznete v konzoli.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Odebrat přístupový klíč" + }, + "passkeyRemoved": { + "message": "Přístupový klíč byl odebrán" + }, + "unassignedItemsBanner": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů a jsou nyní přístupné jen v konzoli správce. Přiřaďte tyto položky do kolekce z konzole pro správce, aby byly viditelné." + }, + "unassignedItemsBannerSelfHost": { + "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 665470b512..79178bc9d5 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 750b2e1170..d808d97412 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -709,7 +709,7 @@ "message": "Vis indstillinger i kontekstmenuen" }, "contextMenuItemDesc": { - "message": "Brug et sekundært klik for at få adgang til adgangskodegenerering og matchende logins til hjemmesiden." + "message": "Brug et sekundært klik for at tilgå adgangskodegenerering og matchende logins til webstedet." }, "contextMenuItemDescAlt": { "message": "Brug et sekundært klik for at få adgang til adgangskodegenerering og matchende logins til webstedet. Gælder alle indloggede konti." @@ -1033,7 +1033,7 @@ "message": "Server URL" }, "apiUrl": { - "message": "API server URL" + "message": "API-server URL" }, "webVaultUrl": { "message": "Web-boks server URL" @@ -1574,7 +1574,7 @@ "message": "Advarsel: Dette er en ikke-sikret HTTP side, og alle indsendte oplysninger kan potentielt ses og ændres af andre. Dette login blev oprindeligt gemt på en sikker (HTTPS) side." }, "insecurePageWarningFillPrompt": { - "message": "Do you still wish to fill this login?" + "message": "Ønsker dette login stadig udfyldt?" }, "autofillIframeWarning": { "message": "Formularen hostes af et andet domæne end URI'en for det gemte login. Vælg OK for at autoudfylde alligevel, eller Afbryd for at stoppe." @@ -1712,7 +1712,7 @@ "message": "Biometri mislykkedes" }, "biometricsFailedDesc": { - "message": "Biometri kan ikke fuldføres, overvej at bruge en hovedadgangskode eller logge ud og ind igen. Fortsætter problemet, kontakt Bitwarden-supporten." + "message": "Biometri kan ikke gennemføres. Overvej at bruge en hovedadgangskode eller at logge ud. Fortsætter problemet, kontakt Bitwarden-supporten." }, "nativeMessaginPermissionErrorTitle": { "message": "Tilladelse ikke givet" @@ -2027,7 +2027,7 @@ "message": "Minutter" }, "vaultTimeoutPolicyInEffect": { - "message": "Organisationspolitikker har sat maks. tilladt boks-timeout. til $HOURS$ time(r) og $MINUTES$ minut(ter).", + "message": "Organisationspolitikkerne har fastsat den maksimalt tilladte boks-timeout til $HOURS$ time(r) og $MINUTES$ minut(ter).", "placeholders": { "hours": { "content": "$1", @@ -2314,7 +2314,7 @@ "message": "Sådan autoudfyldes" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "Vælg et emne fra denne skærm, brug genvejen $COMMAND$ eller udforsk andre valgmuligheder i Indstillinger.", "placeholders": { "command": { "content": "$1", @@ -2323,7 +2323,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "Vælg et emne fra denne skærm eller udforsk andre valgmuligheder i Indstillinger." }, "gotIt": { "message": "Forstået" @@ -2700,7 +2700,7 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Start DUO og følg trinene for at fuldføre indlogningen." + "message": "Start Duo og følg trinnene for at fuldføre indlogningen." }, "duoRequiredForAccount": { "message": "Duo-totrinsindlogning kræves for kontoen." @@ -2712,7 +2712,7 @@ "message": "Pop ud-udvidelse" }, "launchDuo": { - "message": "Start DUO" + "message": "Start Duo" }, "importFormatError": { "message": "Data er ikke korrekt formateret. Tjek importfilen og forsøg igen." @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Fejl under import. Tjek konsollen for detaljer.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Fjern adgangsnøgle" + }, + "passkeyRemoved": { + "message": "Adgangsnøgle fjernet" + }, + "unassignedItemsBanner": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen og er kun tilgængelige via Adminkonsollen. Føj disse emner til en samling fra Adminkonsollen for at gøre dem synlige." + }, + "unassignedItemsBannerSelfHost": { + "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen og vil kun være tilgængelige via Admin-konsollen. Tildel disse emner til en samling via Admin-konsollen for at gøre dem synlige." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 0406953936..deb92e992d 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2706,7 +2706,7 @@ "message": "Für dein Konto ist die Duo Zwei-Faktor-Authentifizierung erforderlich." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Koppel die Erweiterung ab, um die Anmeldung abzuschließen." }, "popoutExtension": { "message": "Popout-Erweiterung" @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Fehler beim Speichern der Zugangsdaten. Details in der Konsole.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Passkey entfernen" + }, + "passkeyRemoved": { + "message": "Passkey entfernt" + }, + "unassignedItemsBanner": { + "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich. Weise diese Einträge einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + }, + "unassignedItemsBannerSelfHost": { + "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 64014298e2..36b14e447f 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 0defc8aa7c..36e3ce65a8 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -2999,5 +2999,34 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 26af3b5f71..1ac55feb42 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index ddbc3f41c9..cbe214f0b3 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index b55c64c9f7..ee5666f3cc 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Se produjo un error al guardar las credenciales. Revise la consola para obtener detalles.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index b7e8b2419e..ea1758468e 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Eemalda pääsuvõti" + }, + "passkeyRemoved": { + "message": "Pääsuvõti on eemaldatud" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index ac30dc4b28..529a1e8127 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -95,10 +95,10 @@ "message": "Auto-fill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Auto-bete txartela" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Auto-bete nortasuna" }, "generatePasswordCopied": { "message": "Sortu pasahitza (kopiatuta)" @@ -110,19 +110,19 @@ "message": "Bat datozen saio-hasierarik gabe" }, "noCards": { - "message": "No cards" + "message": "Txartelik ez" }, "noIdentities": { - "message": "No identities" + "message": "Nortasunik ez" }, "addLoginMenu": { "message": "Add login" }, "addCardMenu": { - "message": "Add card" + "message": "Gehitu txartela" }, "addIdentityMenu": { - "message": "Add identity" + "message": "Gehitu nortasuna" }, "unlockVaultMenu": { "message": "Desblokeatu kutxa gotorra" @@ -223,10 +223,10 @@ "message": "Bitwarden Laguntza zentroa" }, "communityForums": { - "message": "Explore Bitwarden community forums" + "message": "Esploratu Bitwarden komunitatearen foroak" }, "contactSupport": { - "message": "Contact Bitwarden support" + "message": "Jarri harremanetan Bitwardeneko laguntza taldearekin" }, "sync": { "message": "Sinkronizatu" @@ -269,7 +269,7 @@ "message": "Luzera" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Pasahitzaren gutxieneko luzera" }, "uppercase": { "message": "Letra larria (A-Z)" @@ -1064,7 +1064,7 @@ "message": "Edit browser settings." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Itzalita", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1592,10 +1592,10 @@ "message": "Ezarri pasahitz nagusia" }, "currentMasterPass": { - "message": "Current master password" + "message": "Oraingo pasahitz nagusia" }, "newMasterPass": { - "message": "New master password" + "message": "Pasahitz nagusi berria" }, "confirmNewMasterPass": { "message": "Confirm new master password" @@ -2266,10 +2266,10 @@ "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." }, "resendNotification": { - "message": "Resend notification" + "message": "Berbidali jakinarazpena" }, "viewAllLoginOptions": { - "message": "View all log in options" + "message": "Ikusi erregistro guztiak ezarpenetan" }, "notificationSentDevice": { "message": "A notification has been sent to your device." @@ -2293,13 +2293,13 @@ "message": "Check known data breaches for this password" }, "important": { - "message": "Important:" + "message": "Garrantzitsua:" }, "masterPasswordHint": { "message": "Your master password cannot be recovered if you forget it!" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "$LENGTH$ karaktere gutxienez", "placeholders": { "length": { "content": "$1", @@ -2326,7 +2326,7 @@ "message": "Select an item from this screen, or explore other options in settings." }, "gotIt": { - "message": "Got it" + "message": "Ulertuta" }, "autofillSettings": { "message": "Auto-fill settings" @@ -2359,25 +2359,25 @@ "message": "Logging in on" }, "opensInANewWindow": { - "message": "Opens in a new window" + "message": "Leiho berri batean irekitzen da" }, "deviceApprovalRequired": { "message": "Device approval required. Select an approval option below:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Gogoratu gailu hau" }, "uncheckIfPublicDevice": { "message": "Uncheck if using a public device" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Onartu zure beste gailutik" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Eskatu administratzailearen onarpena" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Onartu pasahitz nagusiarekin" }, "ssoIdentifierRequired": { "message": "Organization SSO identifier is required." @@ -2390,31 +2390,31 @@ "message": "Access denied. You do not have permission to view this page." }, "general": { - "message": "General" + "message": "Orokorra" }, "display": { - "message": "Display" + "message": "Bistaratzea" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Kontua zuzen sortu da!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Administratzailearen onarpena eskatuta" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Zure eskaera zure administratzaileari bidali zaio." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Jakinaraziko zaizu onartzen denean." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Arazoak saioa hasterakoan?" }, "loginApproved": { "message": "Login approved" }, "userEmailMissing": { - "message": "User email missing" + "message": "Erabiltzailearen emaila falta da" }, "deviceTrusted": { "message": "Device trusted" @@ -2540,19 +2540,19 @@ "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "Inportatzen...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { - "message": "Data successfully imported!", + "message": "Datuak zuzen inportatu dira!", "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "Errorea gertatu da inportatzean. Begiratu xehetasunak kontsolan.", "description": "Notification message for when an import has failed." }, "importNetworkError": { - "message": "Network error encountered during import.", + "message": "Sareko errorea gertatu da inportatzerakoan.", "description": "Notification message for when an import has failed due to a network error." }, "aliasDomain": { @@ -2602,11 +2602,11 @@ "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "Ez dago elementurik erakusteko", "description": "Text to show in overlay if there are no matching items" }, "newItem": { - "message": "New item", + "message": "Elementu berria", "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { @@ -2618,32 +2618,32 @@ "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "Piztu" }, "ignore": { - "message": "Ignore" + "message": "Ezikusi" }, "importData": { - "message": "Import data", + "message": "Inportatu datuak", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Errorea inportatzerakoan" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Inportatzen saiatu zaren datuekin arazo bat egon da. Mesedez, konpondu ondoren adierazten diren akatsak eta saiatu berriro." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Konpondu beheko akatsak eta saiatu berriro." }, "description": { - "message": "Description" + "message": "Deskribapena" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Datuak zuzen inportatu dira" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Guztira $AMOUNT$ elementu inportatu dira.", "placeholders": { "amount": { "content": "$1", @@ -2652,7 +2652,7 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Saiatu berriro" }, "verificationRequiredForActionSetPinToContinue": { "message": "Verification required for this action. Set a PIN to continue." @@ -2661,10 +2661,10 @@ "message": "Set PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Egiaztatu biometria erabiliz" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Baieztapenaren zain" }, "couldNotCompleteBiometrics": { "message": "Could not complete biometrics." @@ -2673,13 +2673,13 @@ "message": "Need a different method?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Erabili pasahitz nagusia" }, "usePin": { - "message": "Use PIN" + "message": "Erabili PIN kodea" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Erabili biometria" }, "enterVerificationCodeSentToEmail": { "message": "Enter the verification code that was sent to your email." @@ -2688,10 +2688,10 @@ "message": "Resend code" }, "total": { - "message": "Total" + "message": "Guztira" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "$ORGANIZATION$(e)ra datuak inportatzen ari zara. Zure datuak erakunde horretako kideekin parteka daitezke. Jarraitu nahi duzu?", "placeholders": { "organization": { "content": "$1", @@ -2810,7 +2810,7 @@ "message": "You do not have a matching login for this site." }, "confirm": { - "message": "Confirm" + "message": "Berretsi" }, "savePasskey": { "message": "Save passkey" @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 373fc5a8d0..669eb151f4 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 343c22d5d0..17aea532ba 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -11,7 +11,7 @@ "description": "Extension description" }, "loginOrCreateNewAccount": { - "message": "Käytä salattua holviasi kirjautumalla sisään tai tai luo uusi tili." + "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." }, "createAccount": { "message": "Luo tili" @@ -802,7 +802,7 @@ "message": "Lue lisää" }, "authenticatorKeyTotp": { - "message": "Todennusaavain (TOTP)" + "message": "Todennusavain (TOTP)" }, "verificationCodeTotp": { "message": "Todennuskoodi (TOTP)" @@ -2989,15 +2989,27 @@ "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Käyttäjätiedot on tallennettu!", + "message": "Käyttäjätiedot tallennettiin!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Käyttäjätiedot on päivitetty!", + "message": "Käyttäjätiedot päivitettiin!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { "message": "Virhe tallennettaessa käyttäjätietoja. Näet isätietoja hallinnasta.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Poista suojausavain" + }, + "passkeyRemoved": { + "message": "Suojausavain poistettiin" + }, + "unassignedItemsBanner": { + "message": "Huomautus: Organisaatioiden kokoelmiin määrittämättömät kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + }, + "unassignedItemsBannerSelfHost": { + "message": "Huomautus: 2.5.2024 alkaen kokoelmiin määrittämättömät organisaatioiden kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 2a09430c27..42d5060e28 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 73e00ba489..6cced1cb0d 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Erreur lors de l'enregistrement des identifiants. Consultez la console pour plus de détails.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Retirer la clé d'identification (passkey)" + }, + "passkeyRemoved": { + "message": "Clé d'identification (passkey) retirée" + }, + "unassignedItemsBanner": { + "message": "Notice : les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres et sont uniquement accessibles via la console d'administration. Assignez ces éléments à une collection à partir de la console d'administration pour les rendre visibles." + }, + "unassignedItemsBannerSelfHost": { + "message": "Remarque : au 2 mai 2024, les éléments d'organisation non assignés ne sont plus visibles dans votre vue Tous les coffres sur tous les appareils et sont uniquement accessibles via la Console d'administration. Assignez ces éléments à une collection à partir de la Console d'administration pour les rendre visibles." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 4a37361f29..023e03b834 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index d902be6af0..1e633f5eb9 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 767edcfb95..44f645bc47 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index b79477d83c..e74a72bc4f 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index fe4292d9c0..cadd72a475 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Hiba történt a hitelesítések mentésekor. A részletekért ellenőrizzük a konzolt.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Jelszó eltávolítása" + }, + "passkeyRemoved": { + "message": "A jelszó eltávolításra került." + }, + "unassignedItemsBanner": { + "message": "Megjegyzés: A nem hozzá nem rendelt szervezeti elemek már nem láthatók az Összes széf nézetben és csak az Adminisztrátori konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátor konzolból, hogy láthatóvá tegyük azokat." + }, + "unassignedItemsBannerSelfHost": { + "message": "Figyelmeztetés: 2024. május 2-án a nem hozzá rendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak a Felügyeleti konzolon keresztül lesznek elérhetők. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyük azokat." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 1b3e7d4e36..9907a7520c 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -20,7 +20,7 @@ "message": "Masuk" }, "enterpriseSingleSignOn": { - "message": "Sistem Masuk Tunggal Perusahaan" + "message": "SSO Perusahaan" }, "cancel": { "message": "Batal" @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index ac6bcc9cb5..65a5a1ad04 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Errore durante il salvataggio delle credenziali. Controlla la console per più dettagli.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Rimuovi passkey" + }, + "passkeyRemoved": { + "message": "Passkey rimossa" + }, + "unassignedItemsBanner": { + "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + }, + "unassignedItemsBannerSelfHost": { + "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 990775c084..05fb7fe5de 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "資格情報の保存中にエラーが発生しました。詳細はコンソールを確認してください。", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "パスキーを削除" + }, + "passkeyRemoved": { + "message": "パスキーを削除しました" + }, + "unassignedItemsBanner": { + "message": "注意: 割り当てられていない組織項目は、すべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + }, + "unassignedItemsBannerSelfHost": { + "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index fdcd46bc3c..d67b88ba9c 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 4a37361f29..023e03b834 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 20fb2ea458..61cfadc762 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 394534d6b0..c71fbdf7a8 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index fe73a8a28a..0fc146c250 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Klaida išsaugant kredencialus. Išsamesnės informacijos patikrink konsolėje.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Pašalinti slaptaraktį" + }, + "passkeyRemoved": { + "message": "Pašalintas slaptaraktis" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 83f3de2556..efac417556 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Kļūda piekļuves informācijas saglabāšanā. Jāpārbauda, vai konsolē ir izvērstāka informācija.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Noņemt piekļuves atslēgu" + }, + "passkeyRemoved": { + "message": "Piekļuves atslēga noņemta" + }, + "unassignedItemsBanner": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + }, + "unassignedItemsBannerSelfHost": { + "message": "Jāņem vērā: 2024. gada 2. maijā nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index c2d1006694..9b66e6f0d6 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 7ddbe00732..f9f37b2511 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 4a37361f29..023e03b834 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index d7a8345a23..82d847ff0f 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 4a37361f29..023e03b834 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index c28b99b7c2..13d59c4546 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Passkey verwijderen" + }, + "passkeyRemoved": { + "message": "Passkey verwijderd" + }, + "unassignedItemsBanner": { + "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluisjes en zijn alleen toegankelijk via de Admin Console. Om deze items zichtbaar te maken, moet je ze toewijzen aan een collectie via de Admin Console." + }, + "unassignedItemsBannerSelfHost": { + "message": "Kennisgeving: Vanaf 2 mei 2024 zijn niet-toegewezen organisatie-items op geen enkel apparaat meer zichtbaar in de weergave van alle kluisjes en alleen toegankelijk via de Admin Console. Je kunt deze items in het Admin Console aan een collectie toewijzen om ze zichtbaar te maken." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 4a37361f29..023e03b834 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 4a37361f29..023e03b834 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e4c3b7b171..e4b97ec956 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Błąd podczas zapisywania danych logowania. Sprawdź konsolę, aby uzyskać szczegóły.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Usuń passkey" + }, + "passkeyRemoved": { + "message": "Passkey został usunięty" + }, + "unassignedItemsBanner": { + "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy i są dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + }, + "unassignedItemsBannerSelfHost": { + "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 3445a3ff5f..a4e0688e3d 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index a532ac5029..c35531e445 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2913,7 +2913,7 @@ "message": "Mudar de conta" }, "switchAccounts": { - "message": "Mudar de contas" + "message": "Mudar de conta" }, "switchToAccount": { "message": "Mudar para conta" @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Erro ao guardar as credenciais. Verifique a consola para obter detalhes.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remover chave de acesso" + }, + "passkeyRemoved": { + "message": "Chave de acesso removida" + }, + "unassignedItemsBanner": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + }, + "unassignedItemsBannerSelfHost": { + "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de ser visíveis na vista Todos os cofres e só estarão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 4851add018..885d70ca93 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index fb4e2abaac..69d9ca200f 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -688,10 +688,10 @@ "message": "Запрос на обновление пароля логина при обнаружении изменений на сайте. Применяется ко всем авторизованным аккаунтам." }, "enableUsePasskeys": { - "message": "Запрос на сохранение и использование ключей доступа" + "message": "Запрос на сохранение и использование passkey" }, "usePasskeysDesc": { - "message": "Запрос на сохранение новых ключей или в авторизация с ключами, хранящимися в вашем хранилище. Применяется ко всем авторизованным аккаунтам." + "message": "Запрос на сохранение новых passkey или в авторизация с passkey, хранящимися в вашем хранилище. Применяется ко всем авторизованным аккаунтам." }, "notificationChangeDesc": { "message": "Обновить этот пароль в Bitwarden?" @@ -2786,25 +2786,25 @@ "message": "Подтвердите пароль к файлу" }, "typePasskey": { - "message": "Ключ доступа" + "message": "Passkey" }, "passkeyNotCopied": { - "message": "Ключ доступа не будет скопирован" + "message": "Passkey не будет скопирован" }, "passkeyNotCopiedAlert": { - "message": "Ключ доступа не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" + "message": "Passkey не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Необходима верификация со стороны инициирующего сайта. Для аккаунтов без мастер-пароля эта возможность пока не реализована." }, "logInWithPasskey": { - "message": "Войти с ключом доступа?" + "message": "Войти с passkey?" }, "passkeyAlreadyExists": { - "message": "Для данного приложения уже существует ключ доступа." + "message": "Для данного приложения уже существует passkey." }, "noPasskeysFoundForThisApplication": { - "message": "Для данного приложения ключей доступа не найдено." + "message": "Для данного приложения не найден passkey." }, "noMatchingPasskeyLogin": { "message": "У вас нет подходящего логина для этого сайта." @@ -2813,28 +2813,28 @@ "message": "Подтвердить" }, "savePasskey": { - "message": "Сохранить ключ доступа" + "message": "Сохранить passkey" }, "savePasskeyNewLogin": { - "message": "Сохранить ключ доступа как новый логин" + "message": "Сохранить passkey как новый логин" }, "choosePasskey": { - "message": "Выберите логин, для которого будет сохранен данный ключ доступа" + "message": "Выберите логин, для которого будет сохранен данный passkey" }, "passkeyItem": { - "message": "Ключ доступа элемента" + "message": "Элемент passkey" }, "overwritePasskey": { - "message": "Перезаписать ключ доступа?" + "message": "Перезаписать passkey?" }, "overwritePasskeyAlert": { - "message": "Этот элемент уже содержит ключ доступа. Вы уверены, что хотите перезаписать текущий ключ?" + "message": "Этот элемент уже содержит passkey. Вы уверены, что хотите перезаписать текущий passkey?" }, "featureNotSupported": { "message": "Функция пока не поддерживается" }, "yourPasskeyIsLocked": { - "message": "Для использования ключа доступа необходима аутентификация. Для продолжения работы подтвердите свою личность." + "message": "Для использования passkey необходима аутентификация. Для продолжения работы подтвердите свою личность." }, "multifactorAuthenticationCancelled": { "message": "Многофакторная аутентификация отменена" @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Ошибка сохранения учетных данных. Проверьте консоль для получения подробной информации.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Удалить passkey" + }, + "passkeyRemoved": { + "message": "Passkey удален" + }, + "unassignedItemsBanner": { + "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + }, + "unassignedItemsBannerSelfHost": { + "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index ca466ef7bb..fb026226bb 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 382dc82245..a7948d78f3 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Chyba pri ukladaní prihlasovacích údajov. Viac informácii nájdete v konzole.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Odstrániť prístupový kľúč" + }, + "passkeyRemoved": { + "message": "Prístupový kľúč bol odstránený" + }, + "unassignedItemsBanner": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky Trezory a sú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." + }, + "unassignedItemsBannerSelfHost": { + "message": "Upozornenie: 2. mája nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky Trezory a budú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index e11a56acde..2fac491c9c 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index d598263e91..6ec1b6181b 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Грешка при чувању акредитива. Проверите конзолу за детаље.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Уклонити приступачни кључ" + }, + "passkeyRemoved": { + "message": "Приступачни кључ је уклоњен" + }, + "unassignedItemsBanner": { + "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + }, + "unassignedItemsBannerSelfHost": { + "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у приказу Сви сефови и биће доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 082fbac350..d798b98ea0 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2931,7 +2931,7 @@ "message": "active" }, "locked": { - "message": "locked" + "message": "låst" }, "unlocked": { "message": "unlocked" @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 4a37361f29..023e03b834 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 011b7983d4..827ca72854 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 391fef8ddc..cd0e12e6b0 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Kimlik bilgileri kaydedilirken hata oluştu. Ayrıntılar için konsolu kontrol edin.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 98dabb597d..4820860de2 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Помилка збереження облікових даних. Перегляньте подробиці в консолі.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Вилучити ключ доступу" + }, + "passkeyRemoved": { + "message": "Ключ доступу вилучено" + }, + "unassignedItemsBanner": { + "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + }, + "unassignedItemsBannerSelfHost": { + "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі в поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 3a7cf5a794..234e60e756 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index fec2dcc2d9..519313df81 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1500,7 +1500,7 @@ "message": "无效 PIN 码。" }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "无效的 PIN 输入尝试次数过多,正在退出登录。" + "message": "无效的 PIN 输入尝试次数过多,正在注销。" }, "unlockWithBiometrics": { "message": "使用生物识别解锁" @@ -1742,7 +1742,7 @@ "message": "Bitwarden 将不会询问是否为这些域名保存登录信息。您必须刷新页面才能使更改生效。" }, "excludedDomainsDescAlt": { - "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。您必须刷新页面才能使更改生效。" + "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。必须刷新页面才能使更改生效。" }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ 不是一个有效的域名", @@ -2314,7 +2314,7 @@ "message": "如何自动填充" }, "autofillSelectInfoWithCommand": { - "message": "从此界面选择一个项目,使用快捷方式 $COMMAND$,或探索设置中的其他选项。", + "message": "从此界面选择一个项目,使用快捷键 $COMMAND$,或探索设置中的其他选项。", "placeholders": { "command": { "content": "$1", @@ -2335,10 +2335,10 @@ "message": "自动填充键盘快捷键" }, "autofillShortcutNotSet": { - "message": "未设置自动填充快捷方式。请在浏览器设置中更改此设置。" + "message": "未设置自动填充快捷键。可在浏览器的设置中更改它。" }, "autofillShortcutText": { - "message": "自动填充快捷方式为: $COMMAND$。在浏览器设置中更改此项。", + "message": "自动填充快捷键为:$COMMAND$。可在浏览器的设置中更改它。", "placeholders": { "command": { "content": "$1", @@ -2928,7 +2928,7 @@ "message": "已达到账户上限。请注销一个账户后再添加其他账户。" }, "active": { - "message": "已生效" + "message": "活动的" }, "locked": { "message": "已锁定" @@ -2961,7 +2961,7 @@ } }, "commonImportFormats": { - "message": "通用格式", + "message": "常规格式", "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { @@ -2969,7 +2969,7 @@ "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "忽略此设置可能会导致 Bitwarden 自动填充菜单与浏览器自带功能产生冲突。", + "message": "忽略此选项可能会导致 Bitwarden 自动填充菜单与浏览器自带功能产生冲突。", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "保存凭据时出错。检查控制台以获取详细信息。", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "移除通行密钥" + }, + "passkeyRemoved": { + "message": "通行密钥已移除" + }, + "unassignedItemsBanner": { + "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + }, + "unassignedItemsBannerSelfHost": { + "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index fdd6f4639b..b6f1ff574a 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -1058,7 +1058,7 @@ "message": "適用於所有已登入的帳戶。" }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "關閉你的瀏覽器內建密碼管理器設定以避免衝突。" }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "編輯瀏覽器設定" @@ -1168,7 +1168,7 @@ "message": "在每個登入資料旁顯示一個可辨識的圖片。" }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "在每次登入時旁邊顯示可識別的圖片。適用於所有已登入的帳號。" }, "enableBadgeCounter": { "message": "顯示圖示計數器" @@ -2314,7 +2314,7 @@ "message": "如何自動填入" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "從此畫面中選擇一個項目;使用捷徑 $COMMAND$,或在設定中探索其他選項。", "placeholders": { "command": { "content": "$1", @@ -2323,7 +2323,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "從此畫面中選擇一個項目,或在設定中探索其他選項。" }, "gotIt": { "message": "我知道了" @@ -2524,15 +2524,15 @@ "description": "Toggling an expand/collapse state." }, "filelessImport": { - "message": "Import your data to Bitwarden?", + "message": "匯入你的資料至 Bitwarden?", "description": "Default notification title for triggering a fileless import." }, "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", + "message": "保護你的 LastPass 資料並匯入至 Bitwarden?", "description": "LastPass specific notification title for triggering a fileless import." }, "lpCancelFilelessImport": { - "message": "Save as unencrypted file", + "message": "儲存為未加密的檔案", "description": "LastPass specific notification button text for cancelling a fileless import." }, "startFilelessImport": { @@ -2548,7 +2548,7 @@ "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "匯入時發生錯誤。檢查控制台以了解詳細資訊。", "description": "Notification message for when an import has failed." }, "importNetworkError": { @@ -2655,7 +2655,7 @@ "message": "再試一次" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "此操作需要驗證。設定 PIN 碼以繼續。" }, "setPin": { "message": "設定 PIN 碼" @@ -2667,7 +2667,7 @@ "message": "正在等待確認" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "無法完成生物辨識。" }, "needADifferentMethod": { "message": "需要不同的方法嗎?" @@ -2682,7 +2682,7 @@ "message": "用生物識別" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "輸入傳送到你的電子郵件的驗證碼。" }, "resendCode": { "message": "重新傳送驗證碼" @@ -2700,7 +2700,7 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "啟動 Duo 並依照步驟完成登入。" }, "duoRequiredForAccount": { "message": "Duo two-step login is required for your account." @@ -2999,5 +2999,17 @@ "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "unassignedItemsBanner": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." } } diff --git a/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts index bd96a211ba..c18fd1a112 100644 --- a/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-request-service.factory.ts @@ -18,17 +18,25 @@ import { factory, } from "../../../platform/background/service-factories/factory-options"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; type AuthRequestServiceFactoryOptions = FactoryOptions; export type AuthRequestServiceInitOptions = AuthRequestServiceFactoryOptions & AppIdServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & - StateServiceInitOptions; + StateProviderInitOptions; export function authRequestServiceFactory( cache: { authRequestService?: AuthRequestServiceAbstraction } & CachedServices, @@ -41,9 +49,11 @@ export function authRequestServiceFactory( async () => new AuthRequestService( await appIdServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts index fa52ca6231..f600efa18d 100644 --- a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts @@ -23,13 +23,18 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory"; +import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory"; + type AuthServiceFactoryOptions = FactoryOptions; export type AuthServiceInitOptions = AuthServiceFactoryOptions & + AccountServiceInitOptions & MessagingServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & - StateServiceInitOptions; + StateServiceInitOptions & + TokenServiceInitOptions; export function authServiceFactory( cache: { authService?: AbstractAuthService } & CachedServices, @@ -41,10 +46,12 @@ export function authServiceFactory( opts, async () => new AuthService( + await accountServiceFactory(cache, opts), await messagingServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await stateServiceFactory(cache, opts), + await tokenServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts index 5916f38441..cac6f9bbe8 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts @@ -39,9 +39,13 @@ import { platformUtilsServiceFactory, } from "../../../platform/background/service-factories/platform-utils-service.factory"; import { - StateServiceInitOptions, - stateServiceFactory, -} from "../../../platform/background/service-factories/state-service.factory"; + StateProviderInitOptions, + stateProviderFactory, +} from "../../../platform/background/service-factories/state-provider.factory"; +import { + SecureStorageServiceInitOptions, + secureStorageServiceFactory, +} from "../../../platform/background/service-factories/storage-service.factory"; import { UserDecryptionOptionsServiceInitOptions, @@ -55,11 +59,12 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor CryptoFunctionServiceInitOptions & CryptoServiceInitOptions & EncryptServiceInitOptions & - StateServiceInitOptions & AppIdServiceInitOptions & DevicesApiServiceInitOptions & I18nServiceInitOptions & PlatformUtilsServiceInitOptions & + StateProviderInitOptions & + SecureStorageServiceInitOptions & UserDecryptionOptionsServiceInitOptions; export function deviceTrustCryptoServiceFactory( @@ -76,11 +81,12 @@ export function deviceTrustCryptoServiceFactory( await cryptoFunctionServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await encryptServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), await appIdServiceFactory(cache, opts), await devicesApiServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), + await secureStorageServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts), ), ); diff --git a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts index 5fd1866c83..c602acadae 100644 --- a/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/key-connector-service.factory.ts @@ -27,10 +27,15 @@ import { LogServiceInitOptions, } from "../../../platform/background/service-factories/log-service.factory"; import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory"; type KeyConnectorServiceFactoryOptions = FactoryOptions & { @@ -40,13 +45,15 @@ type KeyConnectorServiceFactoryOptions = FactoryOptions & { }; export type KeyConnectorServiceInitOptions = KeyConnectorServiceFactoryOptions & - StateServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & LogServiceInitOptions & OrganizationServiceInitOptions & - KeyGenerationServiceInitOptions; + KeyGenerationServiceInitOptions & + StateProviderInitOptions; export function keyConnectorServiceFactory( cache: { keyConnectorService?: AbstractKeyConnectorService } & CachedServices, @@ -58,7 +65,8 @@ export function keyConnectorServiceFactory( opts, async () => new KeyConnectorService( - await stateServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), @@ -66,6 +74,7 @@ export function keyConnectorServiceFactory( await organizationServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts), opts.keyConnectorServiceOptions.logoutCallback, + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/login-email-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-email-service.factory.ts new file mode 100644 index 0000000000..6e98a9a886 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/login-email-service.factory.ts @@ -0,0 +1,28 @@ +import { LoginEmailServiceAbstraction, LoginEmailService } from "@bitwarden/auth/common"; + +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type LoginEmailServiceFactoryOptions = FactoryOptions; + +export type LoginEmailServiceInitOptions = LoginEmailServiceFactoryOptions & + StateProviderInitOptions; + +export function loginEmailServiceFactory( + cache: { loginEmailService?: LoginEmailServiceAbstraction } & CachedServices, + opts: LoginEmailServiceInitOptions, +): Promise { + return factory( + cache, + "loginEmailService", + opts, + async () => new LoginEmailService(await stateProviderFactory(cache, opts)), + ); +} diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index 2cc4692ca9..f184072cce 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -59,6 +59,7 @@ import { PasswordStrengthServiceInitOptions, } from "../../../tools/background/service_factories/password-strength-service.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; import { authRequestServiceFactory, AuthRequestServiceInitOptions, @@ -71,6 +72,10 @@ import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, } from "./key-connector-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory"; import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory"; import { @@ -81,6 +86,8 @@ import { type LoginStrategyServiceFactoryOptions = FactoryOptions; export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & CryptoServiceInitOptions & ApiServiceInitOptions & TokenServiceInitOptions & @@ -111,6 +118,8 @@ export function loginStrategyServiceFactory( opts, async () => new LoginStrategyService( + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts b/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts new file mode 100644 index 0000000000..a2f9052a3f --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/master-password-service.factory.ts @@ -0,0 +1,42 @@ +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; + +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type MasterPasswordServiceFactoryOptions = FactoryOptions; + +export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions & + StateProviderInitOptions; + +export function internalMasterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise { + return factory( + cache, + "masterPasswordService", + opts, + async () => new MasterPasswordService(await stateProviderFactory(cache, opts)), + ); +} + +export async function masterPasswordServiceFactory( + cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, + opts: MasterPasswordServiceInitOptions, +): Promise { + return (await internalMasterPasswordServiceFactory( + cache, + opts, + )) as MasterPasswordServiceAbstraction; +} diff --git a/apps/browser/src/auth/background/service-factories/token-service.factory.ts b/apps/browser/src/auth/background/service-factories/token-service.factory.ts index 25c30460f0..ba42998209 100644 --- a/apps/browser/src/auth/background/service-factories/token-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/token-service.factory.ts @@ -1,6 +1,10 @@ import { TokenService as AbstractTokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; +import { + EncryptServiceInitOptions, + encryptServiceFactory, +} from "../../../platform/background/service-factories/encrypt-service.factory"; import { FactoryOptions, CachedServices, @@ -10,6 +14,14 @@ import { GlobalStateProviderInitOptions, globalStateProviderFactory, } from "../../../platform/background/service-factories/global-state-provider.factory"; +import { + KeyGenerationServiceInitOptions, + keyGenerationServiceFactory, +} from "../../../platform/background/service-factories/key-generation-service.factory"; +import { + LogServiceInitOptions, + logServiceFactory, +} from "../../../platform/background/service-factories/log-service.factory"; import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, @@ -29,7 +41,10 @@ export type TokenServiceInitOptions = TokenServiceFactoryOptions & SingleUserStateProviderInitOptions & GlobalStateProviderInitOptions & PlatformUtilsServiceInitOptions & - SecureStorageServiceInitOptions; + SecureStorageServiceInitOptions & + KeyGenerationServiceInitOptions & + EncryptServiceInitOptions & + LogServiceInitOptions; export function tokenServiceFactory( cache: { tokenService?: AbstractTokenService } & CachedServices, @@ -45,6 +60,9 @@ export function tokenServiceFactory( await globalStateProviderFactory(cache, opts), (await platformUtilsServiceFactory(cache, opts)).supportsSecureStorage(), await secureStorageServiceFactory(cache, opts), + await keyGenerationServiceFactory(cache, opts), + await encryptServiceFactory(cache, opts), + await logServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index e8be9099ca..a8b67b21ca 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -31,6 +31,11 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { + internalMasterPasswordServiceFactory, + MasterPasswordServiceInitOptions, +} from "./master-password-service.factory"; import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { userDecryptionOptionsServiceFactory, @@ -46,6 +51,8 @@ type UserVerificationServiceFactoryOptions = FactoryOptions; export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryOptions & StateServiceInitOptions & CryptoServiceInitOptions & + AccountServiceInitOptions & + MasterPasswordServiceInitOptions & I18nServiceInitOptions & UserVerificationApiServiceInitOptions & UserDecryptionOptionsServiceInitOptions & @@ -66,6 +73,8 @@ export function userVerificationServiceFactory( new UserVerificationService( await stateServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), + await internalMasterPasswordServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index 77d9741056..9a0423fca3 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -6,6 +6,7 @@ import { Subject, firstValueFrom, map, switchMap, takeUntil } from "rxjs"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -32,6 +33,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private location: Location, private router: Router, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, + private authService: AuthService, ) {} get accountLimit() { @@ -42,13 +44,14 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { return this.accountSwitcherService.SPECIAL_ADD_ACCOUNT_ID; } - get availableAccounts$() { - return this.accountSwitcherService.availableAccounts$; - } - - get currentAccount$() { - return this.accountService.activeAccount$; - } + readonly availableAccounts$ = this.accountSwitcherService.availableAccounts$; + readonly currentAccount$ = this.accountService.activeAccount$.pipe( + switchMap((a) => + a == null + ? null + : this.authService.activeAccountStatus$.pipe(map((s) => ({ ...a, status: s }))), + ), + ); async ngOnInit() { const availableVaultTimeoutActions = await firstValueFrom( diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 1c7f93bf30..643c37b9aa 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Observable, combineLatest, switchMap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { UserId } from "@bitwarden/common/types/guid"; @@ -29,12 +30,14 @@ export class CurrentAccountComponent { private router: Router, private location: Location, private route: ActivatedRoute, + private authService: AuthService, ) { this.currentAccount$ = combineLatest([ this.accountService.activeAccount$, this.avatarService.avatarColor$, + this.authService.activeAccountStatus$, ]).pipe( - switchMap(async ([account, avatarColor]) => { + switchMap(async ([account, avatarColor, accountStatus]) => { if (account == null) { return null; } @@ -42,7 +45,7 @@ export class CurrentAccountComponent { id: account.id, name: account.name || account.email, email: account.email, - status: account.status, + status: accountStatus, avatarColor, }; diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts index f02a8ee201..fe04bee20e 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.spec.ts @@ -1,7 +1,8 @@ import { matches, mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs"; +import { BehaviorSubject, ReplaySubject, firstValueFrom, of, timeout } from "rxjs"; import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -12,22 +13,29 @@ import { UserId } from "@bitwarden/common/types/guid"; import { AccountSwitcherService } from "./account-switcher.service"; describe("AccountSwitcherService", () => { - const accountsSubject = new BehaviorSubject>(null); - const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); + let accountsSubject: BehaviorSubject>; + let activeAccountSubject: BehaviorSubject<{ id: UserId } & AccountInfo>; + let authStatusSubject: ReplaySubject>; const accountService = mock(); const avatarService = mock(); const messagingService = mock(); const environmentService = mock(); const logService = mock(); + const authService = mock(); let accountSwitcherService: AccountSwitcherService; beforeEach(() => { jest.resetAllMocks(); + accountsSubject = new BehaviorSubject>(null); + activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null); + authStatusSubject = new ReplaySubject>(1); + // Use subject to allow for easy updates accountService.accounts$ = accountsSubject; accountService.activeAccount$ = activeAccountSubject; + authService.authStatuses$ = authStatusSubject; accountSwitcherService = new AccountSwitcherService( accountService, @@ -35,48 +43,59 @@ describe("AccountSwitcherService", () => { messagingService, environmentService, logService, + authService, ); }); + afterEach(() => { + accountsSubject.complete(); + activeAccountSubject.complete(); + authStatusSubject.complete(); + }); + describe("availableAccounts$", () => { - it("should return all accounts and an add account option when accounts are less than 5", async () => { - const user1AccountInfo: AccountInfo = { + it("should return all logged in accounts and an add account option when accounts are less than 5", async () => { + const accountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", - status: AuthenticationStatus.Unlocked, }; avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); - accountsSubject.next({ - "1": user1AccountInfo, - } as Record); - - activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "1" as UserId })); + accountsSubject.next({ ["1" as UserId]: accountInfo, ["2" as UserId]: accountInfo }); + authStatusSubject.next({ + ["1" as UserId]: AuthenticationStatus.Unlocked, + ["2" as UserId]: AuthenticationStatus.Locked, + }); + activeAccountSubject.next(Object.assign(accountInfo, { id: "1" as UserId })); const accounts = await firstValueFrom( accountSwitcherService.availableAccounts$.pipe(timeout(20)), ); - expect(accounts).toHaveLength(2); + expect(accounts).toHaveLength(3); expect(accounts[0].id).toBe("1"); expect(accounts[0].isActive).toBeTruthy(); - - expect(accounts[1].id).toBe("addAccount"); + expect(accounts[1].id).toBe("2"); expect(accounts[1].isActive).toBeFalsy(); + + expect(accounts[2].id).toBe("addAccount"); + expect(accounts[2].isActive).toBeFalsy(); }); it.each([5, 6])( "should return only accounts if there are %i accounts", async (numberOfAccounts) => { const seedAccounts: Record = {}; + const seedStatuses: Record = {}; for (let i = 0; i < numberOfAccounts; i++) { seedAccounts[`${i}` as UserId] = { email: `test${i}@email.com`, name: "Test User ${i}", - status: AuthenticationStatus.Unlocked, }; + seedStatuses[`${i}` as UserId] = AuthenticationStatus.Unlocked; } avatarService.getUserAvatarColor$.mockReturnValue(of("#cccccc")); accountsSubject.next(seedAccounts); + authStatusSubject.next(seedStatuses); activeAccountSubject.next( Object.assign(seedAccounts["1" as UserId], { id: "1" as UserId }), ); @@ -89,6 +108,26 @@ describe("AccountSwitcherService", () => { }); }, ); + + it("excludes logged out accounts", async () => { + const user1AccountInfo: AccountInfo = { + name: "Test User 1", + email: "", + }; + accountsSubject.next({ ["1" as UserId]: user1AccountInfo }); + authStatusSubject.next({ ["1" as UserId]: AuthenticationStatus.LoggedOut }); + accountsSubject.next({ + "1": user1AccountInfo, + } as Record); + + const accounts = await firstValueFrom( + accountSwitcherService.availableAccounts$.pipe(timeout(20)), + ); + + // Add account only + expect(accounts).toHaveLength(1); + expect(accounts[0].id).toBe("addAccount"); + }); }); describe("selectAccount", () => { diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index 32ebee7c75..a73ec3e1f6 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -11,6 +11,7 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -48,25 +49,27 @@ export class AccountSwitcherService { private messagingService: MessagingService, private environmentService: EnvironmentService, private logService: LogService, + authService: AuthService, ) { this.availableAccounts$ = combineLatest([ - this.accountService.accounts$, + accountService.accounts$, + authService.authStatuses$, this.accountService.activeAccount$, ]).pipe( - switchMap(async ([accounts, activeAccount]) => { - const accountEntries = Object.entries(accounts).filter( - ([_, account]) => account.status !== AuthenticationStatus.LoggedOut, + switchMap(async ([accounts, accountStatuses, activeAccount]) => { + const loggedInIds = Object.keys(accounts).filter( + (id: UserId) => accountStatuses[id] !== AuthenticationStatus.LoggedOut, ); // Accounts shouldn't ever be more than ACCOUNT_LIMIT but just in case do a greater than - const hasMaxAccounts = accountEntries.length >= this.ACCOUNT_LIMIT; + const hasMaxAccounts = loggedInIds.length >= this.ACCOUNT_LIMIT; const options: AvailableAccount[] = await Promise.all( - accountEntries.map(async ([id, account]) => { + loggedInIds.map(async (id: UserId) => { return { - name: account.name ?? account.email, - email: account.email, + name: accounts[id].name ?? accounts[id].email, + email: accounts[id].email, id: id, server: (await this.environmentService.getEnvironment(id))?.getHostname(), - status: account.status, + status: accountStatuses[id], isActive: id === activeAccount?.id, avatarColor: await firstValueFrom( this.avatarService.getUserAvatarColor$(id as UserId), diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts index a1f79cd457..214a43efb7 100644 --- a/apps/browser/src/auth/popup/hint.component.ts +++ b/apps/browser/src/auth/popup/hint.component.ts @@ -2,8 +2,8 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { HintComponent as BaseHintComponent } from "@bitwarden/angular/auth/components/hint.component"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -20,9 +20,9 @@ export class HintComponent extends BaseHintComponent { apiService: ApiService, logService: LogService, private route: ActivatedRoute, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginService); + super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); super.onSuccessfulSubmit = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/auth/popup/home.component.html b/apps/browser/src/auth/popup/home.component.html index f70a4c6d03..8e23d96c49 100644 --- a/apps/browser/src/auth/popup/home.component.html +++ b/apps/browser/src/auth/popup/home.component.html @@ -30,7 +30,7 @@ diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index 1360e6c8a6..db83736be8 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -1,14 +1,13 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AccountSwitcherService } from "./account-switching/services/account-switcher.service"; @@ -29,38 +28,32 @@ export class HomeComponent implements OnInit, OnDestroy { constructor( protected platformUtilsService: PlatformUtilsService, - private stateService: StateService, private formBuilder: FormBuilder, private router: Router, private i18nService: I18nService, private environmentService: EnvironmentService, - private loginService: LoginService, + private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, ) {} async ngOnInit(): Promise { - let savedEmail = this.loginService.getEmail(); - const rememberEmail = this.loginService.getRememberEmail(); + const email = this.loginEmailService.getEmail(); + const rememberEmail = this.loginEmailService.getRememberEmail(); - if (savedEmail != null) { - this.formGroup.patchValue({ - email: savedEmail, - rememberEmail: rememberEmail, - }); + if (email != null) { + this.formGroup.patchValue({ email, rememberEmail }); } else { - savedEmail = await this.stateService.getRememberedEmail(); - if (savedEmail != null) { - this.formGroup.patchValue({ - email: savedEmail, - rememberEmail: true, - }); + const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); + + if (storedEmail != null) { + this.formGroup.patchValue({ email: storedEmail, rememberEmail: true }); } } this.environmentSelector.onOpenSelfHostedSettings .pipe(takeUntil(this.destroyed$)) .subscribe(() => { - this.setFormValues(); + this.setLoginEmailValues(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["environment"]); @@ -76,8 +69,9 @@ export class HomeComponent implements OnInit, OnDestroy { return this.accountSwitcherService.availableAccounts$; } - submit() { + async submit() { this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { this.platformUtilsService.showToast( "error", @@ -87,15 +81,12 @@ export class HomeComponent implements OnInit, OnDestroy { return; } - this.loginService.setEmail(this.formGroup.value.email); - this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); + this.setLoginEmailValues(); + await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); } - setFormValues() { - this.loginService.setEmail(this.formGroup.value.email); - this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); + setLoginEmailValues() { + this.loginEmailService.setEmail(this.formGroup.value.email); + this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); } } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index f2c56a23ae..16c32337cf 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -9,8 +9,10 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -40,6 +42,7 @@ export class LockComponent extends BaseLockComponent { fido2PopoutSessionData$ = fido2PopoutSessionData$(); constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -62,8 +65,10 @@ export class LockComponent extends BaseLockComponent { pinCryptoService: PinCryptoServiceAbstraction, private routerService: BrowserRouterService, biometricStateService: BiometricStateService, + accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, @@ -84,6 +89,7 @@ export class LockComponent extends BaseLockComponent { userVerificationService, pinCryptoService, biometricStateService, + accountService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index a22636389a..52f311ce7b 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -1,17 +1,18 @@ import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { Router } from "@angular/router"; import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -28,10 +29,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv selector: "app-login-via-auth-request", templateUrl: "login-via-auth-request.component.html", }) -export class LoginViaAuthRequestComponent - extends BaseLoginWithDeviceComponent - implements OnInit, OnDestroy -{ +export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { constructor( router: Router, cryptoService: CryptoService, @@ -47,11 +45,12 @@ export class LoginViaAuthRequestComponent anonymousHubService: AnonymousHubService, validationService: ValidationService, stateService: StateService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, syncService: SyncService, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, + accountService: AccountService, private location: Location, ) { super( @@ -69,10 +68,11 @@ export class LoginViaAuthRequestComponent anonymousHubService, validationService, stateService, - loginService, + loginEmailService, deviceTrustCryptoService, authRequestService, loginStrategyService, + accountService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login.component.html index f6ebb747f7..b24a25a0f1 100644 --- a/apps/browser/src/auth/popup/login.component.html +++ b/apps/browser/src/auth/popup/login.component.html @@ -52,7 +52,7 @@ diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 5c302455e6..ff0ee8a392 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -5,9 +5,11 @@ import { firstValueFrom } from "rxjs"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, +} from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -46,7 +48,7 @@ export class LoginComponent extends BaseLoginComponent { formBuilder: FormBuilder, formValidationErrorService: FormValidationErrorsService, route: ActivatedRoute, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -66,7 +68,7 @@ export class LoginComponent extends BaseLoginComponent { formBuilder, formValidationErrorService, route, - loginService, + loginEmailService, ssoLoginService, webAuthnLoginService, ); @@ -77,8 +79,8 @@ export class LoginComponent extends BaseLoginComponent { this.showPasswordless = flagEnabled("showPasswordless"); if (this.showPasswordless) { - this.formGroup.controls.email.setValue(this.loginService.getEmail()); - this.formGroup.controls.rememberEmail.setValue(this.loginService.getRememberEmail()); + this.formGroup.controls.email.setValue(this.loginEmailService.getEmail()); + this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail()); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.validateEmail(); @@ -94,7 +96,7 @@ export class LoginComponent extends BaseLoginComponent { async launchSsoBrowser() { // Save off email for SSO await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); - await this.loginService.saveEmailSettings(); + await this.loginEmailService.saveEmailSettings(); // Generate necessary sso params const passwordOptions: any = { type: "password", diff --git a/apps/browser/src/auth/popup/services/unauth-guard.service.ts b/apps/browser/src/auth/popup/services/unauth-guard.service.ts index 062239a7d3..0fbb4ac9ba 100644 --- a/apps/browser/src/auth/popup/services/unauth-guard.service.ts +++ b/apps/browser/src/auth/popup/services/unauth-guard.service.ts @@ -1,8 +1,5 @@ -import { Injectable } from "@angular/core"; - import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; -@Injectable() export class UnauthGuardService extends BaseUnauthGuardService { protected homepage = "tabs/current"; } diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts index ea1cacc7ac..accde2e9a0 100644 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ b/apps/browser/src/auth/popup/set-password.component.ts @@ -1,65 +1,9 @@ import { Component } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; -import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-set-password", templateUrl: "set-password.component.html", }) -export class SetPasswordComponent extends BaseSetPasswordComponent { - constructor( - apiService: ApiService, - i18nService: I18nService, - cryptoService: CryptoService, - messagingService: MessagingService, - stateService: StateService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - platformUtilsService: PlatformUtilsService, - policyApiService: PolicyApiServiceAbstraction, - policyService: PolicyService, - router: Router, - syncService: SyncService, - route: ActivatedRoute, - organizationApiService: OrganizationApiServiceAbstraction, - organizationUserService: OrganizationUserService, - userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - ssoLoginService: SsoLoginServiceAbstraction, - dialogService: DialogService, - ) { - super( - i18nService, - cryptoService, - messagingService, - passwordGenerationService, - platformUtilsService, - policyApiService, - policyService, - router, - apiService, - syncService, - route, - stateService, - organizationApiService, - organizationUserService, - userDecryptionOptionsService, - ssoLoginService, - dialogService, - ); - } -} +export class SetPasswordComponent extends BaseSetPasswordComponent {} diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 430bd855f1..14df0d1752 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -9,10 +9,12 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -44,8 +46,10 @@ export class SsoComponent extends BaseSsoComponent { environmentService: EnvironmentService, logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, - protected authService: AuthService, + configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, + private authService: AuthService, @Inject(WINDOW) private win: Window, ) { super( @@ -63,6 +67,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index da2c3482fd..98363bc93c 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -7,16 +7,17 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -32,8 +33,6 @@ import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; -const BroadcasterSubscriptionId = "TwoFactorComponent"; - @Component({ selector: "app-two-factor", templateUrl: "two-factor.component.html", @@ -50,18 +49,19 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { platformUtilsService: PlatformUtilsService, private syncService: SyncService, environmentService: EnvironmentService, - private broadcasterService: BroadcasterService, stateService: StateService, route: ActivatedRoute, private messagingService: MessagingService, logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -78,10 +78,12 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -175,8 +177,6 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { this.destroy$.next(); this.destroy$.complete(); - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { document.body.classList.remove("linux-webauthn"); } diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index ac40bb315b..e01e2c5c02 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -1,4 +1,5 @@ import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum"; @@ -113,6 +114,7 @@ type NotificationBackgroundExtensionMessageHandlers = { bgGetEnableChangedPasswordPrompt: () => Promise; bgGetEnableAddedLoginPrompt: () => Promise; bgGetExcludedDomains: () => Promise; + bgGetActiveUserServerConfig: () => Promise; getWebVaultUrlForNotification: () => Promise; }; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 3b05cf57a9..45f095aee9 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -6,6 +6,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; @@ -16,7 +17,7 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/browser-state.service"; +import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { FormData } from "../services/abstractions/autofill.service"; import AutofillService from "../services/autofill.service"; @@ -48,12 +49,13 @@ describe("NotificationBackground", () => { const authService = mock(); const policyService = mock(); const folderService = mock(); - const stateService = mock(); + const stateService = mock(); const userNotificationSettingsService = mock(); const domainSettingsService = mock(); const environmentService = mock(); const logService = mock(); const themeStateService = mock(); + const configService = mock(); beforeEach(() => { notificationBackground = new NotificationBackground( @@ -68,6 +70,7 @@ describe("NotificationBackground", () => { environmentService, logService, themeStateService, + configService, ); }); @@ -717,7 +720,7 @@ describe("NotificationBackground", () => { ); tabSendMessageSpy = jest.spyOn(BrowserApi, "tabSendMessage").mockImplementation(); editItemSpy = jest.spyOn(notificationBackground as any, "editItem"); - setAddEditCipherInfoSpy = jest.spyOn(stateService, "setAddEditCipherInfo"); + setAddEditCipherInfoSpy = jest.spyOn(cipherService, "setAddEditCipherInfo"); openAddEditVaultItemPopoutSpy = jest.spyOn( notificationBackground as any, "openAddEditVaultItemPopout", diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index c14531ee74..9b65e4db0b 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -8,6 +8,8 @@ import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constan import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -64,6 +66,7 @@ export default class NotificationBackground { bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), bgGetExcludedDomains: () => this.getExcludedDomains(), + bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(), }; @@ -79,6 +82,7 @@ export default class NotificationBackground { private environmentService: EnvironmentService, private logService: LogService, private themeStateService: ThemeStateService, + private configService: ConfigService, ) {} async init() { @@ -112,6 +116,13 @@ export default class NotificationBackground { return await firstValueFrom(this.domainSettingsService.neverDomains$); } + /** + * Gets the active user server config from the config service. + */ + async getActiveUserServerConfig(): Promise { + return await firstValueFrom(this.configService.serverConfig$); + } + /** * Checks the notification queue for any messages that need to be sent to the * specified tab. If no tab is specified, the current tab will be used. @@ -589,14 +600,14 @@ export default class NotificationBackground { } /** - * Sets the add/edit cipher info in the state service + * Sets the add/edit cipher info in the cipher service * and opens the add/edit vault item popout. * * @param cipherView - The cipher to edit * @param senderTab - The tab that the message was sent from */ private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) { - await this.stateService.setAddEditCipherInfo({ + await this.cipherService.setAddEditCipherInfo({ cipher: cipherView, collectionIds: cipherView.collectionIds, }); diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index c06df6603b..e65397a62b 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -33,7 +33,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/browser-state.service"; +import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; import { AutofillService } from "../services/abstractions/autofill.service"; import { @@ -72,7 +72,7 @@ describe("OverlayBackground", () => { urls: { icons: "https://icons.bitwarden.com/" }, }), ); - const stateService = mock(); + const stateService = mock(); const autofillSettingsService = mock(); const i18nService = mock(); const platformUtilsService = mock(); @@ -592,7 +592,7 @@ describe("OverlayBackground", () => { beforeEach(() => { sender = mock({ tab: { id: 1 } }); jest - .spyOn(overlayBackground["stateService"], "setAddEditCipherInfo") + .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") .mockImplementation(); jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); }); @@ -600,7 +600,7 @@ describe("OverlayBackground", () => { it("will not open the add edit popout window if the message does not have a login cipher provided", () => { sendExtensionRuntimeMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - expect(overlayBackground["stateService"].setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); }); @@ -621,7 +621,7 @@ describe("OverlayBackground", () => { ); await flushPromises(); - expect(overlayBackground["stateService"].setAddEditCipherInfo).toHaveBeenCalled(); + expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); expect(BrowserApi.sendMessage).toHaveBeenCalledWith( "inlineAutofillMenuRefreshAddEditCipher", ); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 7b43756553..551263525e 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -604,9 +604,7 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the port message */ private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); + void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); } /** @@ -638,13 +636,13 @@ class OverlayBackground implements OverlayBackgroundInterface { cipherView.type = CipherType.Login; cipherView.login = loginView; - await this.stateService.setAddEditCipherInfo({ + await this.cipherService.setAddEditCipherInfo({ cipher: cipherView, collectionIds: cipherView.collectionIds, }); - await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); + await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); } /** diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index d62e485722..c948f7aa94 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -16,10 +16,6 @@ import { logServiceFactory, LogServiceInitOptions, } from "../../../platform/background/service-factories/log-service.factory"; -import { - stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; import { cipherServiceFactory, CipherServiceInitOptions, @@ -44,7 +40,6 @@ type AutoFillServiceOptions = FactoryOptions; export type AutoFillServiceInitOptions = AutoFillServiceOptions & CipherServiceInitOptions & - StateServiceInitOptions & AutofillSettingsServiceInitOptions & TotpServiceInitOptions & EventCollectionServiceInitOptions & @@ -63,7 +58,6 @@ export function autofillServiceFactory( async () => new AutofillService( await cipherServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), await autofillSettingsServiceFactory(cache, opts), await totpServiceFactory(cache, opts), await eventCollectionServiceFactory(cache, opts), diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index f4422e6d7f..8cdfa0f027 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -17,7 +17,7 @@ export default class WebRequestBackground { private authService: AuthService, ) { if (BrowserApi.isManifestVersion(2)) { - this.webRequest = (window as any).chrome.webRequest; + this.webRequest = chrome.webRequest; } this.isFirefox = platformUtilsService.isFirefox(); } diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 760b833044..596d6b7235 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -30,6 +30,7 @@ import { authServiceFactory, AuthServiceInitOptions, } from "../../auth/background/service-factories/auth-service.factory"; +import { KeyConnectorServiceInitOptions } from "../../auth/background/service-factories/key-connector-service.factory"; import { userVerificationServiceFactory } from "../../auth/background/service-factories/user-verification-service.factory"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { autofillSettingsServiceFactory } from "../../autofill/background/service_factories/autofill-settings-service.factory"; @@ -78,7 +79,9 @@ export class ContextMenuClickedHandler { static async mv3Create(cachedServices: CachedServices) { const stateFactory = new StateFactory(GlobalState, Account); - const serviceOptions: AuthServiceInitOptions & CipherServiceInitOptions = { + const serviceOptions: AuthServiceInitOptions & + CipherServiceInitOptions & + KeyConnectorServiceInitOptions = { apiServiceOptions: { logoutCallback: NOT_IMPLEMENTED, }, diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index 8912a8c0ba..b299ddccbf 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -24,6 +24,7 @@ describe("AutofillInit", () => { }, }); autofillInit = new AutofillInit(autofillOverlayContentService); + window.IntersectionObserver = jest.fn(() => mock()); }); afterEach(() => { diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index e6f6468317..2de35dee20 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -99,9 +99,7 @@ class AutofillInit implements AutofillInitInterface { return pageDetails; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - chrome.runtime.sendMessage({ + void chrome.runtime.sendMessage({ command: "collectPageDetailsResponse", tab: message.tab, details: pageDetails, diff --git a/apps/browser/src/autofill/content/autofiller.ts b/apps/browser/src/autofill/content/autofiller.ts index 5f43023d8b..0ca9d37187 100644 --- a/apps/browser/src/autofill/content/autofiller.ts +++ b/apps/browser/src/autofill/content/autofiller.ts @@ -10,7 +10,7 @@ function loadAutofiller() { let pageHref: string = null; let filledThisHref = false; let delayFillTimeout: number; - let doFillInterval: NodeJS.Timeout; + let doFillInterval: number | NodeJS.Timeout; const handleExtensionDisconnect = () => { clearDoFillInterval(); clearDelayFillTimeout(); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index 8c1ef93c32..2bcf4394fd 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -1,3 +1,4 @@ +import { ServerConfig } from "../../../../../libs/common/src/platform/abstractions/config/server-config"; import { AddLoginMessageData, ChangePasswordMessageData, @@ -6,12 +7,7 @@ import AutofillField from "../models/autofill-field"; import { WatchedForm } from "../models/watched-form"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; import { FormData } from "../services/abstractions/autofill.service"; -import { UserSettings } from "../types"; -import { - getFromLocalStorage, - sendExtensionMessage, - setupExtensionDisconnectAction, -} from "../utils"; +import { sendExtensionMessage, setupExtensionDisconnectAction } from "../utils"; interface HTMLElementWithFormOpId extends HTMLElement { formOpId: string; @@ -95,25 +91,17 @@ async function loadNotificationBar() { ); const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt"); const excludedDomains = await sendExtensionMessage("bgGetExcludedDomains"); + const activeUserServerConfig: ServerConfig = await sendExtensionMessage( + "bgGetActiveUserServerConfig", + ); + const activeUserVault = activeUserServerConfig?.environment?.vault; let showNotificationBar = true; - // Look up the active user id from storage - const activeUserIdKey = "activeUserId"; - let activeUserId: string; - - const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey); - if (activeUserStorageValue[activeUserIdKey]) { - activeUserId = activeUserStorageValue[activeUserIdKey]; - } - - // Look up the user's settings from storage - const userSettingsStorageValue = await getFromLocalStorage(activeUserId); - if (userSettingsStorageValue[activeUserId]) { - const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings; + if (activeUserVault) { // Do not show the notification bar on the Bitwarden vault // because they can add logins and change passwords there - if (window.location.origin === userSettings.serverConfig.environment.vault) { + if (window.location.origin === activeUserVault) { showNotificationBar = false; } else { // NeverDomains is a dictionary of domains that the user has chosen to never diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index 5d28bf8397..a730ee1eba 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -139,7 +139,7 @@ function initNotificationBar(message: NotificationBarWindowMessage) { }); }); - window.addEventListener("resize", adjustHeight); + globalThis.addEventListener("resize", adjustHeight); adjustHeight(); } @@ -384,7 +384,7 @@ function setupLogoLink(i18n: Record) { function setNotificationBarTheme() { let theme = notificationBarIframeInitData.theme; if (theme === ThemeType.System) { - theme = window.matchMedia("(prefers-color-scheme: dark)").matches + theme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? ThemeType.Dark : ThemeType.Light; } @@ -393,5 +393,5 @@ function setNotificationBarTheme() { } function postMessageToParent(message: NotificationBarWindowMessage) { - window.parent.postMessage(message, windowMessageOrigin || "*"); + globalThis.parent.postMessage(message, windowMessageOrigin || "*"); } diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts index 0ec7db131c..b7a6f2a39e 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe.service.ts @@ -211,7 +211,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf let borderColor: string; let verifiedTheme = theme; if (verifiedTheme === ThemeType.System) { - verifiedTheme = window.matchMedia("(prefers-color-scheme: dark)").matches + verifiedTheme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches ? ThemeType.Dark : ThemeType.Light; } diff --git a/apps/browser/src/autofill/overlay/pages/list/__snapshots__/autofill-overlay-list.spec.ts.snap b/apps/browser/src/autofill/overlay/pages/list/__snapshots__/autofill-overlay-list.spec.ts.snap index da9a0c53bf..6ee8e737cb 100644 --- a/apps/browser/src/autofill/overlay/pages/list/__snapshots__/autofill-overlay-list.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/pages/list/__snapshots__/autofill-overlay-list.spec.ts.snap @@ -2,9 +2,7 @@ exports[`AutofillOverlayList initAutofillOverlayList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = ` - {{ typeCounts.get(sendType.File) || 0 }} + {{ getSendCount(sends, sendType.File) }} diff --git a/apps/browser/src/tools/popup/send/send-groupings.component.ts b/apps/browser/src/tools/popup/send/send-groupings.component.ts index 25fa67d51a..87d03c4b76 100644 --- a/apps/browser/src/tools/popup/send/send-groupings.component.ts +++ b/apps/browser/src/tools/popup/send/send-groupings.component.ts @@ -18,7 +18,7 @@ import { DialogService } from "@bitwarden/components"; import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service"; +import { BrowserSendStateService } from "../services/browser-send-state.service"; const ComponentId = "SendComponent"; @@ -29,8 +29,6 @@ const ComponentId = "SendComponent"; export class SendGroupingsComponent extends BaseSendComponent { // Header showLeftHeader = true; - // Send Type Calculations - typeCounts = new Map(); // State Handling state: BrowserSendComponentState; private loadedTimeout: number; @@ -43,7 +41,7 @@ export class SendGroupingsComponent extends BaseSendComponent { ngZone: NgZone, policyService: PolicyService, searchService: SearchService, - private stateService: BrowserStateService, + private stateService: BrowserSendStateService, private router: Router, private syncService: SyncService, private changeDetectorRef: ChangeDetectorRef, @@ -65,7 +63,6 @@ export class SendGroupingsComponent extends BaseSendComponent { dialogService, ); super.onSuccessfulLoad = async () => { - this.calculateTypeCounts(); this.selectAll(); }; } @@ -171,22 +168,11 @@ export class SendGroupingsComponent extends BaseSendComponent { } showSearching() { - return ( - this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)) - ); + return this.hasSearched || (!this.searchPending && this.isSearchable); } - private calculateTypeCounts() { - // Create type counts - const typeCounts = new Map(); - this.sends.forEach((s) => { - if (typeCounts.has(s.type)) { - typeCounts.set(s.type, typeCounts.get(s.type) + 1); - } else { - typeCounts.set(s.type, 1); - } - }); - this.typeCounts = typeCounts; + getSendCount(sends: SendView[], type: SendType): number { + return sends.filter((s) => s.type === type).length; } private async saveState() { @@ -194,7 +180,6 @@ export class SendGroupingsComponent extends BaseSendComponent { scrollY: BrowserPopupUtils.getContentScrollY(window), searchText: this.searchText, sends: this.sends, - typeCounts: this.typeCounts, }); await this.stateService.setBrowserSendComponentState(this.state); } @@ -208,9 +193,6 @@ export class SendGroupingsComponent extends BaseSendComponent { if (this.state.sends != null) { this.sends = this.state.sends; } - if (this.state.typeCounts != null) { - this.typeCounts = this.state.typeCounts; - } return true; } diff --git a/apps/browser/src/tools/popup/send/send-type.component.ts b/apps/browser/src/tools/popup/send/send-type.component.ts index 4b27edc043..aca02587de 100644 --- a/apps/browser/src/tools/popup/send/send-type.component.ts +++ b/apps/browser/src/tools/popup/send/send-type.component.ts @@ -19,7 +19,7 @@ import { DialogService } from "@bitwarden/components"; import { BrowserComponentState } from "../../../models/browserComponentState"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service"; +import { BrowserSendStateService } from "../services/browser-send-state.service"; const ComponentId = "SendTypeComponent"; @@ -42,7 +42,7 @@ export class SendTypeComponent extends BaseSendComponent { ngZone: NgZone, policyService: PolicyService, searchService: SearchService, - private stateService: BrowserStateService, + private stateService: BrowserSendStateService, private route: ActivatedRoute, private location: Location, private changeDetectorRef: ChangeDetectorRef, diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts new file mode 100644 index 0000000000..6f0ae1455a --- /dev/null +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.spec.ts @@ -0,0 +1,59 @@ +import { + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/../spec/fake-account-service"; +import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; +import { awaitAsync } from "@bitwarden/common/../spec/utils"; + +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { BrowserComponentState } from "../../../models/browserComponentState"; +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +import { BrowserSendStateService } from "./browser-send-state.service"; + +describe("Browser Send State Service", () => { + let stateProvider: FakeStateProvider; + + let accountService: FakeAccountService; + let stateService: BrowserSendStateService; + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + stateService = new BrowserSendStateService(stateProvider); + }); + + describe("getBrowserSendComponentState", () => { + it("should return BrowserSendComponentState", async () => { + const state = new BrowserSendComponentState(); + state.scrollY = 0; + state.searchText = "test"; + + await stateService.setBrowserSendComponentState(state); + + await awaitAsync(); + + const actual = await stateService.getBrowserSendComponentState(); + expect(actual).toStrictEqual(state); + }); + }); + + describe("getBrowserSendTypeComponentState", () => { + it("should return BrowserComponentState", async () => { + const state = new BrowserComponentState(); + state.scrollY = 0; + state.searchText = "test"; + + await stateService.setBrowserSendTypeComponentState(state); + + await awaitAsync(); + + const actual = await stateService.getBrowserSendTypeComponentState(); + expect(actual).toStrictEqual(state); + }); + }); +}); diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.ts new file mode 100644 index 0000000000..52aeb01a92 --- /dev/null +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.ts @@ -0,0 +1,65 @@ +import { Observable, firstValueFrom } from "rxjs"; + +import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; + +import { BrowserComponentState } from "../../../models/browserComponentState"; +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions"; + +/** Get or set the active user's component state for the Send browser component + */ +export class BrowserSendStateService { + /** Observable that contains the current state for active user Sends including the send data and type counts + * along with the search text and scroll position + */ + browserSendComponentState$: Observable; + + /** Observable that contains the current state for active user Sends that only includes the search text + * and scroll position + */ + browserSendTypeComponentState$: Observable; + + private activeUserBrowserSendComponentState: ActiveUserState; + private activeUserBrowserSendTypeComponentState: ActiveUserState; + + constructor(protected stateProvider: StateProvider) { + this.activeUserBrowserSendComponentState = this.stateProvider.getActive(BROWSER_SEND_COMPONENT); + this.browserSendComponentState$ = this.activeUserBrowserSendComponentState.state$; + + this.activeUserBrowserSendTypeComponentState = this.stateProvider.getActive( + BROWSER_SEND_TYPE_COMPONENT, + ); + this.browserSendTypeComponentState$ = this.activeUserBrowserSendTypeComponentState.state$; + } + + /** Get the active user's browser send component state + * @returns { BrowserSendComponentState } contains the sends and type counts along with the scroll position and search text for the + * send component on the browser + */ + async getBrowserSendComponentState(): Promise { + return await firstValueFrom(this.browserSendComponentState$); + } + + /** Set the active user's browser send component state + * @param { BrowserSendComponentState } value sets the sends along with the scroll position and search text for + * the send component on the browser + */ + async setBrowserSendComponentState(value: BrowserSendComponentState): Promise { + await this.activeUserBrowserSendComponentState.update(() => value); + } + + /** Get the active user's browser component state + * @returns { BrowserComponentState } contains the scroll position and search text for the sends menu on the browser + */ + async getBrowserSendTypeComponentState(): Promise { + return await firstValueFrom(this.browserSendTypeComponentState$); + } + + /** Set the active user's browser component state + * @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser + */ + async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise { + await this.activeUserBrowserSendTypeComponentState.update(() => value); + } +} diff --git a/apps/browser/src/tools/popup/services/key-definitions.spec.ts b/apps/browser/src/tools/popup/services/key-definitions.spec.ts new file mode 100644 index 0000000000..7517771669 --- /dev/null +++ b/apps/browser/src/tools/popup/services/key-definitions.spec.ts @@ -0,0 +1,39 @@ +import { Jsonify } from "type-fest"; + +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +import { BROWSER_SEND_COMPONENT, BROWSER_SEND_TYPE_COMPONENT } from "./key-definitions"; + +describe("Key definitions", () => { + describe("BROWSER_SEND_COMPONENT", () => { + it("should deserialize BrowserSendComponentState", () => { + const keyDef = BROWSER_SEND_COMPONENT; + + const expectedState = { + scrollY: 0, + searchText: "test", + }; + + const result = keyDef.deserializer( + JSON.parse(JSON.stringify(expectedState)) as Jsonify, + ); + + expect(result).toEqual(expectedState); + }); + }); + + describe("BROWSER_SEND_TYPE_COMPONENT", () => { + it("should deserialize BrowserComponentState", () => { + const keyDef = BROWSER_SEND_TYPE_COMPONENT; + + const expectedState = { + scrollY: 0, + searchText: "test", + }; + + const result = keyDef.deserializer(JSON.parse(JSON.stringify(expectedState))); + + expect(result).toEqual(expectedState); + }); + }); +}); diff --git a/apps/browser/src/tools/popup/services/key-definitions.ts b/apps/browser/src/tools/popup/services/key-definitions.ts new file mode 100644 index 0000000000..9b256073f3 --- /dev/null +++ b/apps/browser/src/tools/popup/services/key-definitions.ts @@ -0,0 +1,23 @@ +import { Jsonify } from "type-fest"; + +import { BROWSER_SEND_MEMORY, KeyDefinition } from "@bitwarden/common/platform/state"; + +import { BrowserComponentState } from "../../../models/browserComponentState"; +import { BrowserSendComponentState } from "../../../models/browserSendComponentState"; + +export const BROWSER_SEND_COMPONENT = new KeyDefinition( + BROWSER_SEND_MEMORY, + "browser_send_component", + { + deserializer: (obj: Jsonify) => + BrowserSendComponentState.fromJSON(obj), + }, +); + +export const BROWSER_SEND_TYPE_COMPONENT = new KeyDefinition( + BROWSER_SEND_MEMORY, + "browser_send_type_component", + { + deserializer: (obj: Jsonify) => BrowserComponentState.fromJSON(obj), + }, +); diff --git a/apps/browser/src/tools/popup/settings/export.component.ts b/apps/browser/src/tools/popup/settings/export.component.ts index 70735b5184..b62ed4c517 100644 --- a/apps/browser/src/tools/popup/settings/export.component.ts +++ b/apps/browser/src/tools/popup/settings/export.component.ts @@ -2,7 +2,6 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; -import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -13,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui"; @Component({ selector: "app-export", diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts index 8ffeca72bc..57366ea8c0 100644 --- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts @@ -42,6 +42,7 @@ import { i18nServiceFactory, I18nServiceInitOptions, } from "../../../platform/background/service-factories/i18n-service.factory"; +import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory"; import { stateServiceFactory, StateServiceInitOptions, @@ -81,6 +82,7 @@ export function cipherServiceFactory( await encryptServiceFactory(cache, opts), await cipherFileUploadServiceFactory(cache, opts), await configServiceFactory(cache, opts), + await stateProviderFactory(cache, opts), ), ); } diff --git a/apps/browser/src/vault/background/service_factories/folder-service.factory.ts b/apps/browser/src/vault/background/service_factories/folder-service.factory.ts index 72847a0536..0593dc904c 100644 --- a/apps/browser/src/vault/background/service_factories/folder-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/folder-service.factory.ts @@ -14,11 +14,10 @@ import { i18nServiceFactory, I18nServiceInitOptions, } from "../../../platform/background/service-factories/i18n-service.factory"; -import { stateProviderFactory } from "../../../platform/background/service-factories/state-provider.factory"; import { - stateServiceFactory as stateServiceFactory, - StateServiceInitOptions, -} from "../../../platform/background/service-factories/state-service.factory"; + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; import { cipherServiceFactory, CipherServiceInitOptions } from "./cipher-service.factory"; @@ -28,7 +27,7 @@ export type FolderServiceInitOptions = FolderServiceFactoryOptions & CryptoServiceInitOptions & CipherServiceInitOptions & I18nServiceInitOptions & - StateServiceInitOptions; + StateProviderInitOptions; export function folderServiceFactory( cache: { folderService?: AbstractFolderService } & CachedServices, @@ -43,7 +42,6 @@ export function folderServiceFactory( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await cipherServiceFactory(cache, opts), - await stateServiceFactory(cache, opts), await stateProviderFactory(cache, opts), ), ); diff --git a/apps/browser/src/vault/fido2/content/content-script.ts b/apps/browser/src/vault/fido2/content/content-script.ts index e12c592262..c2fc862f55 100644 --- a/apps/browser/src/vault/fido2/content/content-script.ts +++ b/apps/browser/src/vault/fido2/content/content-script.ts @@ -138,6 +138,7 @@ async function run() { }); } -// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. -// eslint-disable-next-line @typescript-eslint/no-floating-promises -run(); +// Only run the script if the document is an HTML document +if (document.contentType === "text/html") { + void run(); +} diff --git a/apps/browser/src/vault/popup/components/action-buttons.component.ts b/apps/browser/src/vault/popup/components/action-buttons.component.ts index 624789a5c0..b0e7b318d2 100644 --- a/apps/browser/src/vault/popup/components/action-buttons.component.ts +++ b/apps/browser/src/vault/popup/components/action-buttons.component.ts @@ -6,7 +6,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -31,7 +31,7 @@ export class ActionButtonsComponent implements OnInit, OnDestroy { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private eventCollectionService: EventCollectionService, - private totpService: TotpService, + private totpService: TotpServiceAbstraction, private passwordRepromptService: PasswordRepromptService, private billingAccountProfileStateService: BillingAccountProfileStateService, ) {} diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index 81d1b88fd8..323d2ab4f2 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -311,7 +311,7 @@ export class Fido2Component implements OnInit, OnDestroy { } protected async search() { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = await this.searchService.isSearchable(this.searchText); this.searchPending = true; if (this.hasSearched) { this.displayedCiphers = await this.searchService.searchCiphers( diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html index ecdeb9cda7..8ff448b6f7 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html @@ -138,10 +138,20 @@ attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}" >
-
- {{ "typePasskey" | i18n }} - {{ "dateCreated" | i18n }} - {{ cipher.login.fido2Credentials[0].creationDate | date: "short" }} +
+ +
+ {{ "typePasskey" | i18n }} + {{ "dateCreated" | i18n }} + {{ cipher.login.fido2Credentials[0].creationDate | date: "short" }} +
diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index 8e52d44069..a566b054c0 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -11,7 +11,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -68,7 +68,7 @@ export class AddEditComponent extends BaseAddEditComponent { sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( cipherService, @@ -304,7 +304,7 @@ export class AddEditComponent extends BaseAddEditComponent { } private saveCipherState() { - return this.stateService.setAddEditCipherInfo({ + return this.cipherService.setAddEditCipherInfo({ cipher: this.cipher, collectionIds: this.collections == null diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html index c971f6c937..0b2e16d09d 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.html +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.html @@ -36,19 +36,43 @@ - -

{{ autofillCalloutText }}

+ +

+ {{ unassignedItemsBannerService.bannerText$ | async | i18n }} + {{ "unassignedItemsBannerCTAPartOne" | i18n }} + {{ "adminConsole" | i18n }} + {{ "unassignedItemsBannerCTAPartTwo" | i18n }} + {{ "learnMore" | i18n }} +

-

diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index d9cf6550fa..4d2674fd70 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -1,13 +1,16 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom } from "rxjs"; -import { debounceTime, takeUntil } from "rxjs/operators"; +import { Subject, firstValueFrom, from } from "rxjs"; +import { debounceTime, switchMap, takeUntil } from "rxjs/operators"; +import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -53,6 +56,11 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private totpTimeout: number; private loadedTimeout: number; private searchTimeout: number; + private initPageDetailsTimeout: number; + + protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.UnassignedItemsBanner, + ); constructor( private platformUtilsService: PlatformUtilsService, @@ -70,6 +78,8 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, private vaultSettingsService: VaultSettingsService, + private configService: ConfigService, + protected unassignedItemsBannerService: UnassignedItemsBannerService, ) {} async ngOnInit() { @@ -120,8 +130,14 @@ export class CurrentTabComponent implements OnInit, OnDestroy { } this.search$ - .pipe(debounceTime(500), takeUntil(this.destroy$)) - .subscribe(() => this.searchVault()); + .pipe( + debounceTime(500), + switchMap(() => { + return from(this.searchVault()); + }), + takeUntil(this.destroy$), + ) + .subscribe(); const autofillOnPageLoadOrgPolicy = await firstValueFrom( this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$, @@ -232,14 +248,12 @@ export class CurrentTabComponent implements OnInit, OnDestroy { } } - searchVault() { - if (!this.searchService.isSearchable(this.searchText)) { + async searchVault() { + if (!(await this.searchService.isSearchable(this.searchText))) { return; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } }); + await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } }); } closeOnEsc(e: KeyboardEvent) { @@ -303,18 +317,13 @@ export class CurrentTabComponent implements OnInit, OnDestroy { }); if (this.loginCiphers.length) { - void BrowserApi.tabSendMessage(this.tab, { - command: "collectPageDetails", - tab: this.tab, - sender: BroadcasterSubscriptionId, - }); - this.loginCiphers = this.loginCiphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), ); } this.isLoading = this.loaded = true; + this.collectTabPageDetails(); } async goToSettings() { @@ -352,4 +361,19 @@ export class CurrentTabComponent implements OnInit, OnDestroy { this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand"); } } + + private collectTabPageDetails() { + void BrowserApi.tabSendMessage(this.tab, { + command: "collectPageDetails", + tab: this.tab, + sender: BroadcasterSubscriptionId, + }); + + window.clearTimeout(this.initPageDetailsTimeout); + this.initPageDetailsTimeout = window.setTimeout(() => { + if (this.pageDetails.length === 0) { + this.collectTabPageDetails(); + } + }, 250); + } } diff --git a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts index 5e7959b38f..deb4434df4 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-filter.component.ts @@ -1,8 +1,8 @@ import { Location } from "@angular/common"; import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; -import { first } from "rxjs/operators"; +import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; +import { first, switchMap, takeUntil } from "rxjs/operators"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -20,7 +20,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service"; +import { VaultBrowserStateService } from "../../../services/vault-browser-state.service"; import { VaultFilterService } from "../../../services/vault-filter.service"; const ComponentId = "VaultComponent"; @@ -53,7 +53,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { folderCounts = new Map(); collectionCounts = new Map(); typeCounts = new Map(); - searchText: string; state: BrowserGroupingsComponentState; showLeftHeader = true; searchPending = false; @@ -71,6 +70,16 @@ export class VaultFilterComponent implements OnInit, OnDestroy { private hasSearched = false; private hasLoadedAllCiphers = false; private allCiphers: CipherView[] = null; + private destroy$ = new Subject(); + private _searchText$ = new BehaviorSubject(""); + private isSearchable: boolean = false; + + get searchText() { + return this._searchText$.value; + } + set searchText(value: string) { + this._searchText$.next(value); + } constructor( private i18nService: I18nService, @@ -84,8 +93,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private searchService: SearchService, private location: Location, - private browserStateService: BrowserStateService, private vaultFilterService: VaultFilterService, + private vaultBrowserStateService: VaultBrowserStateService, ) { this.noFolderListSize = 100; } @@ -95,7 +104,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { this.showLeftHeader = !( BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox() ); - await this.browserStateService.setBrowserVaultItemsComponentState(null); + await this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null); this.broadcasterService.subscribe(ComponentId, (message: any) => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -120,7 +129,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { const restoredScopeState = await this.restoreState(); // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (params) => { - this.state = await this.browserStateService.getBrowserGroupingComponentState(); + this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState(); if (this.state?.searchText) { this.searchText = this.state.searchText; } else if (params.searchText) { @@ -148,6 +157,15 @@ export class VaultFilterComponent implements OnInit, OnDestroy { BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY); } }); + + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearchable = isSearchable; + }); } ngOnDestroy() { @@ -161,6 +179,8 @@ export class VaultFilterComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.saveState(); this.broadcasterService.unsubscribe(ComponentId); + this.destroy$.next(); + this.destroy$.complete(); } async load() { @@ -181,7 +201,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { async loadCiphers() { this.allCiphers = await this.cipherService.getAllDecrypted(); if (!this.hasLoadedAllCiphers) { - this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); + this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText)); } await this.search(null); this.getCounts(); @@ -210,7 +230,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } const filterDeleted = (c: CipherView) => !c.isDeleted; if (timeout == null) { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; this.ciphers = await this.searchService.searchCiphers( this.searchText, filterDeleted, @@ -223,7 +243,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; if (!this.hasLoadedAllCiphers && !this.hasSearched) { await this.loadCiphers(); } else { @@ -381,9 +401,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy { } showSearching() { - return ( - this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)) - ); + return this.hasSearched || (!this.searchPending && this.isSearchable); } closeOnEsc(e: KeyboardEvent) { @@ -413,11 +431,11 @@ export class VaultFilterComponent implements OnInit, OnDestroy { collections: this.collections, deletedCount: this.deletedCount, }); - await this.browserStateService.setBrowserGroupingComponentState(this.state); + await this.vaultBrowserStateService.setBrowserGroupingsComponentState(this.state); } private async restoreState(): Promise { - this.state = await this.browserStateService.getBrowserGroupingComponentState(); + this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState(); if (this.state == null) { return false; } diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index 96d5fe170b..abb810c04d 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -21,7 +21,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { BrowserComponentState } from "../../../../models/browserComponentState"; import { BrowserApi } from "../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../../platform/services/abstractions/browser-state.service"; +import { VaultBrowserStateService } from "../../../services/vault-browser-state.service"; import { VaultFilterService } from "../../../services/vault-filter.service"; const ComponentId = "VaultItemsComponent"; @@ -59,7 +59,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn private ngZone: NgZone, private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef, - private stateService: BrowserStateService, + private stateService: VaultBrowserStateService, private i18nService: I18nService, private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService, diff --git a/apps/browser/src/vault/popup/components/vault/view.component.ts b/apps/browser/src/vault/popup/components/vault/view.component.ts index d7ef15afb7..a225db0c11 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view.component.ts @@ -20,7 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; @@ -74,7 +74,7 @@ export class ViewComponent extends BaseViewComponent { constructor( cipherService: CipherService, folderService: FolderService, - totpService: TotpService, + totpService: TotpServiceAbstraction, tokenService: TokenService, i18nService: I18nService, cryptoService: CryptoService, diff --git a/apps/browser/src/vault/services/vault-browser-state.service.spec.ts b/apps/browser/src/vault/services/vault-browser-state.service.spec.ts new file mode 100644 index 0000000000..b9369aa826 --- /dev/null +++ b/apps/browser/src/vault/services/vault-browser-state.service.spec.ts @@ -0,0 +1,87 @@ +import { + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/../spec/fake-account-service"; +import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; +import { Jsonify } from "type-fest"; + +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { UserId } from "@bitwarden/common/types/guid"; +import { CipherType } from "@bitwarden/common/vault/enums"; + +import { BrowserComponentState } from "../../models/browserComponentState"; +import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState"; + +import { + VAULT_BROWSER_COMPONENT, + VAULT_BROWSER_GROUPINGS_COMPONENT, + VaultBrowserStateService, +} from "./vault-browser-state.service"; + +describe("Vault Browser State Service", () => { + let stateProvider: FakeStateProvider; + + let accountService: FakeAccountService; + let stateService: VaultBrowserStateService; + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + stateService = new VaultBrowserStateService(stateProvider); + }); + + describe("getBrowserGroupingsComponentState", () => { + it("should return a BrowserGroupingsComponentState", async () => { + await stateService.setBrowserGroupingsComponentState(new BrowserGroupingsComponentState()); + + const actual = await stateService.getBrowserGroupingsComponentState(); + + expect(actual).toBeInstanceOf(BrowserGroupingsComponentState); + }); + + it("should deserialize BrowserGroupingsComponentState", () => { + const sut = VAULT_BROWSER_GROUPINGS_COMPONENT; + + const expectedState = { + deletedCount: 0, + collectionCounts: new Map(), + folderCounts: new Map(), + typeCounts: new Map(), + }; + + const result = sut.deserializer( + JSON.parse(JSON.stringify(expectedState)) as Jsonify, + ); + + expect(result).toEqual(expectedState); + }); + }); + + describe("getBrowserVaultItemsComponentState", () => { + it("should deserialize BrowserComponentState", () => { + const sut = VAULT_BROWSER_COMPONENT; + + const expectedState = { + scrollY: 0, + searchText: "test", + }; + + const result = sut.deserializer(JSON.parse(JSON.stringify(expectedState))); + + expect(result).toEqual(expectedState); + }); + + it("should return a BrowserComponentState", async () => { + const componentState = new BrowserComponentState(); + componentState.scrollY = 0; + componentState.searchText = "test"; + + await stateService.setBrowserVaultItemsComponentState(componentState); + + const actual = await stateService.getBrowserVaultItemsComponentState(); + expect(actual).toStrictEqual(componentState); + }); + }); +}); diff --git a/apps/browser/src/vault/services/vault-browser-state.service.ts b/apps/browser/src/vault/services/vault-browser-state.service.ts new file mode 100644 index 0000000000..a0d55a9d55 --- /dev/null +++ b/apps/browser/src/vault/services/vault-browser-state.service.ts @@ -0,0 +1,65 @@ +import { Observable, firstValueFrom } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { + ActiveUserState, + KeyDefinition, + StateProvider, + VAULT_BROWSER_MEMORY, +} from "@bitwarden/common/platform/state"; + +import { BrowserComponentState } from "../../models/browserComponentState"; +import { BrowserGroupingsComponentState } from "../../models/browserGroupingsComponentState"; + +export const VAULT_BROWSER_GROUPINGS_COMPONENT = new KeyDefinition( + VAULT_BROWSER_MEMORY, + "vault_browser_groupings_component", + { + deserializer: (obj: Jsonify) => + BrowserGroupingsComponentState.fromJSON(obj), + }, +); + +export const VAULT_BROWSER_COMPONENT = new KeyDefinition( + VAULT_BROWSER_MEMORY, + "vault_browser_component", + { + deserializer: (obj: Jsonify) => BrowserComponentState.fromJSON(obj), + }, +); + +export class VaultBrowserStateService { + vaultBrowserGroupingsComponentState$: Observable; + vaultBrowserComponentState$: Observable; + + private activeUserVaultBrowserGroupingsComponentState: ActiveUserState; + private activeUserVaultBrowserComponentState: ActiveUserState; + + constructor(protected stateProvider: StateProvider) { + this.activeUserVaultBrowserGroupingsComponentState = this.stateProvider.getActive( + VAULT_BROWSER_GROUPINGS_COMPONENT, + ); + this.activeUserVaultBrowserComponentState = + this.stateProvider.getActive(VAULT_BROWSER_COMPONENT); + + this.vaultBrowserGroupingsComponentState$ = + this.activeUserVaultBrowserGroupingsComponentState.state$; + this.vaultBrowserComponentState$ = this.activeUserVaultBrowserComponentState.state$; + } + + async getBrowserGroupingsComponentState(): Promise { + return await firstValueFrom(this.vaultBrowserGroupingsComponentState$); + } + + async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise { + await this.activeUserVaultBrowserGroupingsComponentState.update(() => value); + } + + async getBrowserVaultItemsComponentState(): Promise { + return await firstValueFrom(this.vaultBrowserComponentState$); + } + + async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise { + await this.activeUserVaultBrowserComponentState.update(() => value); + } +} diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index 191198691d..d812256fb7 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden – Xestor de contrasinais gratuíto - A secure and free password manager for all of your devices + Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Bitwarden, Inc. é a empresa matriz de 8bit Solutions LLC. -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +NOMEADO MELLOR ADMINISTRADOR DE CONTRASINAIS POR THE VERGE, Ou.S. NEWS & WORLD REPORT, CNET E MÁS. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +Administre, almacene, protexa e comparta contrasinais ilimitados en dispositivos ilimitados desde calquera lugar. Bitwarden ofrece solucións de xestión de contrasinais de código aberto para todos, xa sexa en casa, no traballo ou en mentres estás de viaxe. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +Xere contrasinais seguros, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +Bitwarden Send transmite rapidamente información cifrada --- arquivos e texto sen formato, directamente a calquera persoa. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Bitwarden ofrece plans Teams e Enterprise para empresas para que poida compartir contrasinais de forma segura con colegas. -Why Choose Bitwarden: +Por que elixir Bitwarden? -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Cifrado de clase mundial +Os contrasinais están protexidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing e PBKDF2 XA-256) para que os seus datos permanezan seguros e privados. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +Xerador de contrasinais incorporado +Xere contrasinais fortes, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Traducións Globais +As traducións de Bitwarden existen en 40 idiomas e están a crecer, grazas á nosa comunidade global. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplicacións multiplataforma +Protexa e comparta datos confidenciais dentro da súa Caixa Forte de Bitwarden desde calquera navegador, dispositivo móbil ou sistema operativo de escritorio, e máis. - A secure and free password manager for all of your devices + Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos - Sync and access your vault from multiple devices + Sincroniza e accede á túa caixa forte desde múltiples dispositivos - Manage all your logins and passwords from a secure vault + Xestiona todos os teus usuarios e contrasinais desde unha caixa forte segura - Quickly auto-fill your login credentials into any website that you visit + Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites - Your vault is also conveniently accessible from the right-click menu + A túa caixa forte tamén é facilmente accesible desde o menú de clic dereito - Automatically generate strong, random, and secure passwords + Xera automaticamente contrasinais fortes, aleatorias e seguras - Your information is managed securely using AES-256 bit encryption + A túa información é xestionada de forma segura con cifrado AES de 256 bits diff --git a/apps/cli/package.json b/apps/cli/package.json index 2873be4242..690842d831 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.3.0", + "version": "2024.3.1", "keywords": [ "bitwarden", "password", @@ -71,7 +71,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.13", + "tldts": "6.1.16", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index 98bc926079..d52468139a 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -1,6 +1,10 @@ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -18,6 +22,8 @@ import { CliUtils } from "../../utils"; export class UnlockCommand { constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private stateService: StateService, private cryptoFunctionService: CryptoFunctionService, @@ -45,11 +51,14 @@ export class UnlockCommand { const kdf = await this.stateService.getKdfType(); const kdfConfig = await this.stateService.getKdfConfig(); const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); - const storedKeyHash = await this.cryptoService.getMasterKeyHash(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const storedMasterKeyHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); let passwordValid = false; if (masterKey != null) { - if (storedKeyHash != null) { + if (storedMasterKeyHash != null) { passwordValid = await this.cryptoService.compareAndUpdateKeyHash(password, masterKey); } else { const serverKeyHash = await this.cryptoService.hashMasterKey( @@ -67,7 +76,7 @@ export class UnlockCommand { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); } catch { // Ignore } @@ -75,7 +84,7 @@ export class UnlockCommand { } if (passwordValid) { - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7af40b1ebd..7fbefc10e3 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -18,22 +18,26 @@ import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; @@ -47,6 +51,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { @@ -60,6 +65,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -104,6 +110,7 @@ import { PasswordStrengthServiceAbstraction, } from "@bitwarden/common/tools/password-strength"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; +import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -131,7 +138,6 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; -import { CliConfigService } from "./platform/services/cli-config.service"; import { CliPlatformUtilsService } from "./platform/services/cli-platform-utils.service"; import { ConsoleLogService } from "./platform/services/console-log.service"; import { I18nService } from "./platform/services/i18n.service"; @@ -166,6 +172,7 @@ export class Main { organizationUserService: OrganizationUserService; collectionService: CollectionService; vaultTimeoutService: VaultTimeoutService; + masterPasswordService: InternalMasterPasswordServiceAbstraction; vaultTimeoutSettingsService: VaultTimeoutSettingsService; syncService: SyncService; eventCollectionService: EventCollectionServiceAbstraction; @@ -193,6 +200,7 @@ export class Main { sendProgram: SendProgram; logService: ConsoleLogService; sendService: SendService; + sendStateProvider: SendStateProvider; fileUploadService: FileUploadService; cipherFileUploadService: CipherFileUploadService; keyConnectorService: KeyConnectorService; @@ -214,7 +222,7 @@ export class Main { deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; authRequestService: AuthRequestService; configApiService: ConfigApiServiceAbstraction; - configService: CliConfigService; + configService: ConfigService; accountService: AccountService; globalStateProvider: GlobalStateProvider; singleUserStateProvider: SingleUserStateProvider; @@ -226,6 +234,7 @@ export class Main { stateEventRunnerService: StateEventRunnerService; biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; + providerApiService: ProviderApiServiceAbstraction; constructor() { let p = null; @@ -318,11 +327,16 @@ export class Main { this.accountService, ); + this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); + this.tokenService = new TokenService( this.singleUserStateProvider, this.globalStateProvider, this.platformUtilsService.supportsSecureStorage(), this.secureStorageService, + this.keyGenerationService, + this.encryptService, + this.logService, ); const migrationRunner = new MigrationRunner( @@ -343,9 +357,10 @@ export class Main { migrationRunner, ); - this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); + this.masterPasswordService = new MasterPasswordService(this.stateProvider); this.cryptoService = new CryptoService( + this.masterPasswordService, this.keyGenerationService, this.cryptoFunctionService, this.encryptService, @@ -384,11 +399,14 @@ export class Main { this.fileUploadService = new FileUploadService(this.logService); + this.sendStateProvider = new SendStateProvider(this.stateProvider); + this.sendService = new SendService( this.cryptoService, this.i18nService, this.keyGenerationService, - this.stateService, + this.sendStateProvider, + this.encryptService, ); this.cipherFileUploadService = new CipherFileUploadService( @@ -402,7 +420,7 @@ export class Main { this.sendService, ); - this.searchService = new SearchService(this.logService, this.i18nService); + this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); this.broadcasterService = new BroadcasterService(); @@ -423,7 +441,8 @@ export class Main { this.policyApiService = new PolicyApiService(this.policyService, this.apiService); this.keyConnectorService = new KeyConnectorService( - this.stateService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -431,6 +450,7 @@ export class Main { this.organizationService, this.keyGenerationService, async (expired: boolean) => await this.logout(), + this.stateProvider, ); this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); @@ -451,26 +471,31 @@ export class Main { this.cryptoFunctionService, this.cryptoService, this.encryptService, - this.stateService, this.appIdService, this.devicesApiService, this.i18nService, this.platformUtilsService, + this.stateProvider, + this.secureStorageService, this.userDecryptionOptionsService, ); this.authRequestService = new AuthRequestService( this.appIdService, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, - this.stateService, + this.stateProvider, ); this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( - this.activeUserStateProvider, + this.stateProvider, ); this.loginStrategyService = new LoginStrategyService( + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -494,22 +519,21 @@ export class Main { ); this.authService = new AuthService( + this.accountService, this.messagingService, this.cryptoService, this.apiService, this.stateService, + this.tokenService, ); - this.configApiService = new ConfigApiService(this.apiService, this.authService); + this.configApiService = new ConfigApiService(this.apiService, this.tokenService); - this.configService = new CliConfigService( - this.stateService, + this.configService = new DefaultConfigService( this.configApiService, - this.authService, this.environmentService, this.logService, this.stateProvider, - true, ); this.cipherService = new CipherService( @@ -523,13 +547,13 @@ export class Main { this.encryptService, this.cipherFileUploadService, this.configService, + this.stateProvider, ); this.folderService = new FolderService( this.cryptoService, this.i18nService, this.cipherService, - this.stateService, this.stateProvider, ); @@ -559,6 +583,8 @@ export class Main { this.userVerificationService = new UserVerificationService( this.stateService, this.cryptoService, + this.accountService, + this.masterPasswordService, this.i18nService, this.userVerificationApiService, this.userDecryptionOptionsService, @@ -569,6 +595,8 @@ export class Main { ); this.vaultTimeoutService = new VaultTimeoutService( + this.accountService, + this.masterPasswordService, this.cipherService, this.folderService, this.collectionService, @@ -587,6 +615,8 @@ export class Main { this.avatarService = new AvatarService(this.apiService, this.stateProvider); this.syncService = new SyncService( + this.masterPasswordService, + this.accountService, this.apiService, this.domainSettingsService, this.folderService, @@ -655,7 +685,7 @@ export class Main { this.apiService, this.stateProvider, this.logService, - this.accountService, + this.authService, ); this.eventCollectionService = new EventCollectionService( @@ -663,8 +693,10 @@ export class Main { this.stateProvider, this.organizationService, this.eventUploadService, - this.accountService, + this.authService, ); + + this.providerApiService = new ProviderApiService(this.apiService); } async run() { @@ -693,9 +725,7 @@ export class Main { this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId as UserId), - this.policyService.clear(userId as UserId), this.passwordGenerationService.clear(), - this.providerService.save(null, userId as UserId), ]); await this.stateEventRunnerService.handleEvent("logout", userId as UserId); @@ -710,13 +740,6 @@ export class Main { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); - this.configService.init(); - - const installedVersion = await this.stateService.getInstalledVersion(); - const currentVersion = await this.platformUtilsService.getApplicationVersion(); - if (installedVersion == null || installedVersion !== currentVersion) { - await this.stateService.setInstalledVersion(currentVersion); - } } } diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 4d0d1e5798..76447f769c 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -122,6 +122,8 @@ export class ServeCommand { this.shareCommand = new ShareCommand(this.main.cipherService); this.lockCommand = new LockCommand(this.main.vaultTimeoutService); this.unlockCommand = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index e12b30af2c..8364e0b328 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -49,5 +49,14 @@ }, "unsupportedEncryptedImport": { "message": "Importing encrypted files is currently not supported." + }, + "importUnassignedItemsError": { + "message": "File contains unassigned items." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/cli/src/platform/flags.ts b/apps/cli/src/platform/flags.ts index 4e31e39e99..dc0103e243 100644 --- a/apps/cli/src/platform/flags.ts +++ b/apps/cli/src/platform/flags.ts @@ -7,11 +7,11 @@ import { } from "@bitwarden/common/platform/misc/flags"; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = {} & SharedFlags; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = {} & SharedDevFlags; export function flagEnabled(flag: keyof Flags): boolean { diff --git a/apps/cli/src/platform/services/cli-config.service.ts b/apps/cli/src/platform/services/cli-config.service.ts deleted file mode 100644 index 6faa1b12e8..0000000000 --- a/apps/cli/src/platform/services/cli-config.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NEVER } from "rxjs"; - -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; - -export class CliConfigService extends ConfigService { - // The rxjs timer uses setTimeout/setInterval under the hood, which prevents the node process from exiting - // when the command is finished. Cli should never be alive long enough to use the timer, so we disable it. - protected refreshTimer$ = NEVER; -} diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index a79f3847da..fa71a88f54 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -253,6 +253,8 @@ export class Program { if (!cmd.check) { await this.exitIfNotAuthed(); const command = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, @@ -613,6 +615,8 @@ export class Program { this.processResponse(response, true); } else { const command = new UnlockCommand( + this.main.accountService, + this.main.masterPasswordService, this.main.cryptoService, this.main.stateService, this.main.cryptoFunctionService, diff --git a/apps/cli/src/tools/send/commands/remove-password.command.ts b/apps/cli/src/tools/send/commands/remove-password.command.ts index 1c7289bf08..2613004a8c 100644 --- a/apps/cli/src/tools/send/commands/remove-password.command.ts +++ b/apps/cli/src/tools/send/commands/remove-password.command.ts @@ -18,7 +18,7 @@ export class SendRemovePasswordCommand { try { await this.sendApiService.removePassword(id); - const updatedSend = await this.sendService.get(id); + const updatedSend = await firstValueFrom(this.sendService.get$(id)); const decSend = await updatedSend.decrypt(); const env = await firstValueFrom(this.environmentService.environment$); const webVaultUrl = env.getWebVaultUrl(); diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json index d2b1073812..7a8659feff 100644 --- a/apps/desktop/config/development.json +++ b/apps/desktop/config/development.json @@ -1,7 +1,6 @@ { "devFlags": {}, "flags": { - "showDDGSetting": true, "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json index 39b78094d0..f57c3d9bc3 100644 --- a/apps/desktop/config/production.json +++ b/apps/desktop/config/production.json @@ -1,6 +1,5 @@ { "flags": { - "showDDGSetting": true, "enableCipherKeyEncryption": false } } diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 89170f4cc3..b921cab37b 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -45,9 +45,9 @@ checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "arboard" -version = "3.3.0" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafb29b107435aa276664c1db8954ac27a6e105cdad3c88287a199eb0e313c08" +checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58" dependencies = [ "clipboard-win", "log", @@ -56,22 +56,21 @@ dependencies = [ "objc_id", "parking_lot", "thiserror", - "winapi", "wl-clipboard-rs", "x11rb", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -96,9 +95,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block" @@ -124,12 +123,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bytecount" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" - [[package]] name = "cbc" version = "0.1.2" @@ -141,18 +134,15 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-expr" -version = "0.15.5" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" dependencies = [ "smallvec", "target-lexicon", @@ -164,6 +154,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cipher" version = "0.4.4" @@ -176,13 +172,11 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "4.5.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" dependencies = [ "error-code", - "str-buf", - "winapi", ] [[package]] @@ -222,9 +216,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -241,19 +235,19 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583" +checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" dependencies = [ "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "cxx" -version = "1.0.110" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" +checksum = "ff4dc7287237dd438b926a81a1a5605dad33d286870e5eee2db17bf2bcd9e92a" dependencies = [ "cc", "cxxbridge-flags", @@ -263,9 +257,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.110" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a24f3f5f8eed71936f21e570436f024f5c2e25628f7496aa7ccd03b90109d5" +checksum = "f47c6c8ad7c1a10d3ef0fe3ff6733f4db0d78f08ef0b13121543163ef327058b" dependencies = [ "cc", "codespan-reporting", @@ -273,35 +267,35 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.38", + "syn", ] [[package]] name = "cxxbridge-flags" -version = "1.0.110" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" +checksum = "701a1ac7a697e249cdd8dc026d7a7dafbfd0dbcd8bd24ec55889f2bc13dd6287" [[package]] name = "cxxbridge-macro" -version = "1.0.110" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" +checksum = "b404f596046b0bb2d903a9c786b875a126261b52b7c3a64bbb66382c41c771df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "derive-new" -version = "0.5.9" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -375,19 +369,15 @@ dependencies = [ [[package]] name = "error-code" -version = "2.3.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", -] +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "fixedbitset" @@ -403,24 +393,24 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -429,32 +419,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-macro", @@ -476,19 +466,19 @@ dependencies = [ [[package]] name = "gethostname" -version = "0.3.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -497,9 +487,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gio" @@ -529,16 +519,16 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "glib" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9e86540b5d8402e905ad4ce7d6aa544092131ab564f3102175af176b90a053" +checksum = "01e191cc1af1f35b9699213107068cd3fe05d9816275ac118dc785a0dd8faebf" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "futures-channel", "futures-core", "futures-executor", @@ -556,15 +546,15 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f5897ca27a83e4cdc7b4666850bade0a2e73e17689aabafcc9acddad9d823b8" +checksum = "9972bb91643d589c889654693a4f1d07697fdcb5d104b5c44fb68649ba1bf68d" dependencies = [ "heck", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] @@ -590,27 +580,36 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] [[package]] name = "indexmap" -version = "2.0.2" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -647,26 +646,20 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "winapi", + "windows-targets 0.52.4", ] [[package]] @@ -706,9 +699,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -722,9 +715,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "malloc_buf" @@ -737,18 +730,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "minimal-lexical" @@ -758,20 +742,20 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "napi" -version = "2.13.3" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd063c93b900149304e3ba96ce5bf210cd4f81ef5eb80ded0d100df3e85a3ac0" +checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "ctor", "napi-derive", "napi-sys", @@ -781,29 +765,29 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e" +checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43" [[package]] name = "napi-derive" -version = "2.13.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367" +checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "napi-derive-backend" -version = "1.0.52" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17" +checksum = "ce5126b64f6ad9e28e30e6d15213dd378626b38f556454afebc42f7f02a90902" dependencies = [ "convert_case", "once_cell", @@ -811,29 +795,28 @@ dependencies = [ "quote", "regex", "semver", - "syn 1.0.109", + "syn", ] [[package]] name = "napi-sys" -version = "2.2.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3" +checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" dependencies = [ "libloading", ] [[package]] name = "nix" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "cfg-if", + "cfg_aliases", "libc", - "memoffset", - "pin-utils", ] [[package]] @@ -887,18 +870,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "os_pipe" @@ -945,9 +928,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -957,9 +940,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "ppv-lite86" @@ -978,27 +961,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1044,9 +1027,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", @@ -1056,9 +1039,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1067,9 +1050,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "retry" @@ -1088,11 +1071,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -1142,35 +1125,35 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -1197,32 +1180,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" - -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -1231,9 +1197,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.1.2" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94af52f9402f94aac4948a2518b43359be8d9ce6cd9efc1c4de3b2f7b7e897d6" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", "heck", @@ -1244,57 +1210,56 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.12" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "num_cpus", @@ -1303,14 +1268,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.6" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.20.7", + "toml_edit 0.22.9", ] [[package]] @@ -1322,19 +1287,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "toml_edit" version = "0.21.1" @@ -1343,18 +1295,31 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.5", ] [[package]] name = "tree_magic_mini" -version = "3.0.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d" +checksum = "77ee137597cdb361b55a4746983e4ac1b35ab6024396a419944ad473bb915265" dependencies = [ - "bytecount", "fnv", - "lazy_static", + "home", + "memchr", "nom", "once_cell", "petgraph", @@ -1374,9 +1339,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" @@ -1386,9 +1351,9 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "version-compare" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" @@ -1404,13 +1369,13 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wayland-backend" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ "cc", "downcast-rs", - "nix", + "rustix", "scoped-tls", "smallvec", "wayland-sys", @@ -1418,23 +1383,23 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "bitflags 2.4.1", - "nix", + "bitflags 2.5.0", + "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" -version = "0.31.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1446,7 +1411,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1455,9 +1420,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" dependencies = [ "proc-macro2", "quick-xml", @@ -1506,15 +1471,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "winapi-wsapoll" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1675,18 +1631,27 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" -version = "0.5.17" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] [[package]] name = "wl-clipboard-rs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57af79e973eadf08627115c73847392e6b766856ab8e3844a59245354b23d2fa" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" dependencies = [ "derive-new", "libc", @@ -1704,22 +1669,17 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" dependencies = [ "gethostname", - "nix", - "winapi", - "winapi-wsapoll", + "rustix", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" -dependencies = [ - "nix", -] +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 48536934ee..4b2bc2e905 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -15,20 +15,20 @@ manual_test = [] [dependencies] aes = "=0.8.4" anyhow = "=1.0.80" -arboard = { version = "=3.3.0", default-features = false, features = ["wayland-data-control"] } +arboard = { version = "=3.3.2", default-features = false, features = ["wayland-data-control"] } base64 = "=0.22.0" cbc = { version = "=0.1.2", features = ["alloc"] } -napi = { version = "=2.13.3", features = ["async"] } -napi-derive = "=2.13.0" +napi = { version = "=2.16.0", features = ["async"] } +napi-derive = "=2.16.0" rand = "=0.8.5" retry = "=2.0.0" scopeguard = "=1.2.0" sha2 = "=0.10.8" -thiserror = "=1.0.51" +thiserror = "=1.0.58" typenum = "=1.17.0" [build-dependencies] -napi-build = "=2.0.1" +napi-build = "=2.1.2" [target.'cfg(windows)'.dependencies] widestring = "=1.0.2" diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 5ce5ef948f..4f0d05581c 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -24,7 +24,7 @@ "**/node_modules/argon2/package.json", "**/node_modules/argon2/lib/binding/napi-v3/argon2.node" ], - "electronVersion": "28.2.8", + "electronVersion": "28.3.1", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", @@ -203,7 +203,7 @@ "si", "sk", "sl", - "sr", + "sr-cyrl", "sv", "te", "th", @@ -228,6 +228,7 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "snap": { + "summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.", "autoStart": true, "base": "core22", "confinement": "strict", diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index 3ea3cb7943..e2961eb9ee 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -19,18 +19,16 @@ }, "devDependencies": { "@tsconfig/node16": "1.0.4", - "@types/node": "18.19.19", + "@types/node": "18.19.29", "@types/node-ipc": "9.2.0", "typescript": "4.7.4" } }, "../../../libs/common": { - "name": "@bitwarden/common", "version": "0.0.0", "license": "GPL-3.0" }, "../../../libs/node": { - "name": "@bitwarden/node", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -57,9 +55,9 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } @@ -79,9 +77,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.10.tgz", + "integrity": "sha512-PiaIWIoPvO6qm6t114ropMCagj6YAF24j9OkCA2mJDXFnlionEwhsBCJ8yek4aib575BI3OkART/90WsgHgLWw==" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", @@ -99,9 +97,9 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" }, "node_modules/@types/node": { - "version": "18.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.19.tgz", - "integrity": "sha512-qqV6hSy9zACEhQUy5CEGeuXAZN0fNjqLWRIvOXOwdFYhFoKBiY08VKR5kgchr90+TitLVhpUEb54hk4bYaArUw==", + "version": "18.19.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz", + "integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==", "dependencies": { "undici-types": "~5.26.4" } @@ -116,9 +114,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "bin": { "acorn": "bin/acorn" }, @@ -127,9 +125,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "engines": { "node": ">=0.4.0" } @@ -217,9 +215,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index 3a7fe3b958..c572613119 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@tsconfig/node16": "1.0.4", - "@types/node": "18.19.19", + "@types/node": "18.19.29", "@types/node-ipc": "9.2.0", "typescript": "4.7.4" }, diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6b01918d6b..4bb0ab2d93 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.3.1", + "version": "2024.4.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 4e6af9bff4..5f59530d8c 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -3,6 +3,7 @@ import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs"; import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -20,10 +21,11 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; -import { flagEnabled } from "../../platform/flags"; +import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @Component({ @@ -41,7 +43,6 @@ export class SettingsComponent implements OnInit { themeOptions: any[]; clearClipboardOptions: any[]; supportsBiometric: boolean; - additionalBiometricSettingsText: string; showAlwaysShowDock = false; requireEnableTray = false; showDuckDuckGoIntegrationOption = false; @@ -61,6 +62,7 @@ export class SettingsComponent implements OnInit { showAppPreferences = true; currentUserEmail: string; + currentUserId: UserId; availableVaultTimeoutActions$: Observable; vaultTimeoutPolicyCallout: Observable<{ @@ -122,6 +124,8 @@ export class SettingsComponent implements OnInit { private userVerificationService: UserVerificationServiceAbstraction, private desktopSettingsService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private desktopAutofillSettingsService: DesktopAutofillSettingsService, + private authRequestService: AuthRequestServiceAbstraction, ) { const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; @@ -145,7 +149,7 @@ export class SettingsComponent implements OnInit { this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc"); // DuckDuckGo browser is only for macos initially - this.showDuckDuckGoIntegrationOption = flagEnabled("showDDGSetting") && isMac; + this.showDuckDuckGoIntegrationOption = isMac; this.vaultTimeoutOptions = [ // { name: i18nService.t('immediately'), value: 0 }, @@ -207,6 +211,7 @@ export class SettingsComponent implements OnInit { return; } this.currentUserEmail = await this.stateService.getEmail(); + this.currentUserId = (await this.stateService.getUserId()) as UserId; this.availableVaultTimeoutActions$ = this.refreshTimeoutSettings$.pipe( switchMap(() => this.vaultTimeoutSettingsService.availableVaultTimeoutActions$()), @@ -249,7 +254,8 @@ export class SettingsComponent implements OnInit { requirePasswordOnStart: await firstValueFrom( this.biometricStateService.requirePasswordOnStart$, ), - approveLoginRequests: (await this.stateService.getApproveLoginRequests()) ?? false, + approveLoginRequests: + (await this.authRequestService.getAcceptAuthRequests(this.currentUserId)) ?? false, clearClipboard: await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$), minimizeOnCopyToClipboard: await this.stateService.getMinimizeOnCopyToClipboard(), enableFavicons: await firstValueFrom(this.domainSettingsService.showFavicons$), @@ -262,11 +268,12 @@ export class SettingsComponent implements OnInit { enableBrowserIntegration: await this.stateService.getEnableBrowserIntegration(), enableBrowserIntegrationFingerprint: await this.stateService.getEnableBrowserIntegrationFingerprint(), + enableDuckDuckGoBrowserIntegration: await firstValueFrom( + this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$, + ), enableHardwareAcceleration: await firstValueFrom( this.desktopSettingsService.hardwareAcceleration$, ), - enableDuckDuckGoBrowserIntegration: - await this.stateService.getEnableDuckDuckGoBrowserIntegration(), theme: await firstValueFrom(this.themeStateService.selectedTheme$), locale: await firstValueFrom(this.i18nService.userSetLocale$), }; @@ -280,10 +287,6 @@ export class SettingsComponent implements OnInit { this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop; this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); - this.additionalBiometricSettingsText = - this.biometricText === "unlockWithTouchId" - ? "additionalTouchIdSettings" - : "additionalWindowsHelloSettings"; this.previousVaultTimeout = this.form.value.vaultTimeout; this.refreshTimeoutSettings$ @@ -640,7 +643,7 @@ export class SettingsComponent implements OnInit { } async saveDdgBrowserIntegration() { - await this.stateService.setEnableDuckDuckGoBrowserIntegration( + await this.desktopAutofillSettingsService.setEnableDuckDuckGoBrowserIntegration( this.form.value.enableDuckDuckGoBrowserIntegration, ); @@ -668,7 +671,10 @@ export class SettingsComponent implements OnInit { } async updateApproveLoginRequests() { - await this.stateService.setApproveLoginRequests(this.form.value.approveLoginRequests); + await this.authRequestService.setAcceptAuthRequests( + this.form.value.approveLoginRequests, + this.currentUserId, + ); } ngOnDestroy() { @@ -697,4 +703,15 @@ export class SettingsComponent implements OnInit { throw new Error("Unsupported platform"); } } + + get additionalBiometricSettingsText() { + switch (this.platformUtilsService.getDevice()) { + case DeviceType.MacOsDesktop: + return "additionalTouchIdSettings"; + case DeviceType.WindowsDesktop: + return "additionalWindowsHelloSettings"; + default: + throw new Error("Unsupported platform"); + } + } } diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index fa396ab313..b2b44e6b21 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -26,12 +26,13 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -120,6 +121,7 @@ export class AppComponent implements OnInit, OnDestroy { private accountCleanUpInProgress: { [userId: string]: boolean } = {}; constructor( + private masterPasswordService: MasterPasswordServiceAbstraction, private broadcasterService: BroadcasterService, private folderService: InternalFolderService, private syncService: SyncService, @@ -147,7 +149,7 @@ export class AppComponent implements OnInit, OnDestroy { private modalService: ModalService, private keyConnectorService: KeyConnectorService, private userVerificationService: UserVerificationService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, @@ -265,7 +267,7 @@ export class AppComponent implements OnInit, OnDestroy { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.updateAppMenu(); - this.configService.triggerServerConfigFetch(); + await this.configService.ensureConfigFetched(); } break; case "openSettings": @@ -408,8 +410,9 @@ export class AppComponent implements OnInit, OnDestroy { (await this.authService.getAuthStatus(message.userId)) === AuthenticationStatus.Locked; const forcedPasswordReset = - (await this.stateService.getForceSetPasswordReason({ userId: message.userId })) != - ForceSetPasswordReason.None; + (await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(message.userId), + )) != ForceSetPasswordReason.None; if (locked) { this.messagingService.send("locked", { userId: message.userId }); } else if (forcedPasswordReset) { @@ -583,10 +586,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.collectionService.clear(userBeingLoggedOut); await this.passwordGenerationService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); - await this.policyService.clear(userBeingLoggedOut); - await this.keyConnectorService.clear(); await this.biometricStateService.logout(userBeingLoggedOut as UserId); - await this.providerService.save(null, userBeingLoggedOut as UserId); await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId); @@ -609,7 +609,6 @@ export class AppComponent implements OnInit, OnDestroy { // This must come last otherwise the logout will prematurely trigger // a process reload before all the state service user data can be cleaned up if (userBeingLoggedOut === preLogoutActiveUserId) { - this.searchService.clearIndex(); this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 499300086d..4e39ab0029 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -4,6 +4,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -91,6 +92,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private router: Router, private tokenService: TokenService, private environmentService: EnvironmentService, + private loginEmailService: LoginEmailServiceAbstraction, ) {} async ngOnInit(): Promise { @@ -137,7 +139,10 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { async addAccount() { this.close(); - await this.stateService.setRememberedEmail(null); + + this.loginEmailService.setRememberEmail(false); + await this.loginEmailService.saveEmailSettings(); + await this.router.navigate(["/login"]); await this.stateService.setActiveUser(null); } diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index f45d530edd..d1a83d468c 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -11,7 +11,6 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt. import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -36,7 +35,6 @@ export class InitService { private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, private encryptService: EncryptService, - private configService: ConfigService, @Inject(DOCUMENT) private document: Document, ) {} @@ -55,23 +53,9 @@ export class InitService { const htmlEl = this.win.document.documentElement; htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString()); this.themingService.applyThemeChangesTo(this.document); - let installAction = null; - const installedVersion = await this.stateService.getInstalledVersion(); - const currentVersion = await this.platformUtilsService.getApplicationVersion(); - if (installedVersion == null) { - installAction = "install"; - } else if (installedVersion !== currentVersion) { - installAction = "update"; - } - - if (installAction != null) { - await this.stateService.setInstalledVersion(currentVersion); - } const containerService = new ContainerService(this.cryptoService, this.encryptService); containerService.attachToGlobal(this.win); - - this.configService.init(); }; } } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index bed8f21bbe..8e412d4977 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,8 +1,8 @@ -import { APP_INITIALIZER, InjectionToken, NgModule } from "@angular/core"; +import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, - STATE_FACTORY, STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, @@ -12,15 +12,16 @@ import { WINDOW, SUPPORTS_SECURE_STORAGE, SYSTEM_THEME_OBSERVABLE, + SafeInjectionToken, + STATE_FACTORY, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -53,6 +54,7 @@ import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vau import { DialogService } from "@bitwarden/components"; import { LoginGuard } from "../../auth/guards/login.guard"; +import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { Account } from "../../models/account"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { ElectronCryptoService } from "../../platform/services/electron-crypto.service"; @@ -76,148 +78,183 @@ import { DesktopFileDownloadService } from "./desktop-file-download.service"; import { InitService } from "./init.service"; import { RendererCryptoFunctionService } from "./renderer-crypto-function.service"; -const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); +const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); + +// Desktop has its own Account definition which must be used in its StateService +const DESKTOP_STATE_FACTORY = new SafeInjectionToken>( + "DESKTOP_STATE_FACTORY", +); + +/** + * Provider definitions used in the ngModule. + * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. + * If you need help please ask for it, do NOT change the type of this array. + */ +const safeProviders: SafeProvider[] = [ + safeProvider(InitService), + safeProvider(NativeMessagingService), + safeProvider(SearchBarService), + safeProvider(LoginGuard), + safeProvider(DialogService), + safeProvider({ + provide: APP_INITIALIZER as SafeInjectionToken<() => void>, + useFactory: (initService: InitService) => initService.init(), + deps: [InitService], + multi: true, + }), + safeProvider({ + provide: DESKTOP_STATE_FACTORY, + useValue: new StateFactory(GlobalState, Account), + }), + safeProvider({ + provide: STATE_FACTORY, + useValue: null, + }), + safeProvider({ + provide: RELOAD_CALLBACK, + useValue: null, + }), + safeProvider({ + provide: LogServiceAbstraction, + useClass: ElectronLogRendererService, + deps: [], + }), + safeProvider({ + provide: PlatformUtilsServiceAbstraction, + useClass: ElectronPlatformUtilsService, + deps: [I18nServiceAbstraction, MessagingServiceAbstraction], + }), + safeProvider({ + // We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid + // the TokenService having to inject the PlatformUtilsService which introduces a + // circular dependency on Desktop only. + provide: SUPPORTS_SECURE_STORAGE, + useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, + }), + safeProvider({ + provide: I18nServiceAbstraction, + useClass: I18nRendererService, + deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], + }), + safeProvider({ + provide: MessagingServiceAbstraction, + useClass: ElectronRendererMessagingService, + deps: [BroadcasterServiceAbstraction], + }), + safeProvider({ + provide: AbstractStorageService, + useClass: ElectronRendererStorageService, + deps: [], + }), + safeProvider({ + provide: SECURE_STORAGE, + useClass: ElectronRendererSecureStorageService, + deps: [], + }), + safeProvider({ provide: MEMORY_STORAGE, useClass: MemoryStorageService, deps: [] }), + safeProvider({ + provide: OBSERVABLE_MEMORY_STORAGE, + useClass: MemoryStorageServiceForStateProviders, + deps: [], + }), + safeProvider({ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService }), + safeProvider({ + provide: SystemServiceAbstraction, + useClass: SystemService, + deps: [ + MessagingServiceAbstraction, + PlatformUtilsServiceAbstraction, + RELOAD_CALLBACK, + StateServiceAbstraction, + AutofillSettingsServiceAbstraction, + VaultTimeoutSettingsService, + BiometricStateService, + ], + }), + safeProvider({ + provide: StateServiceAbstraction, + useClass: ElectronStateService, + deps: [ + AbstractStorageService, + SECURE_STORAGE, + MEMORY_STORAGE, + LogService, + DESKTOP_STATE_FACTORY, + AccountServiceAbstraction, + EnvironmentService, + TokenService, + MigrationRunner, + STATE_SERVICE_USE_CACHE, + ], + }), + safeProvider({ + provide: FileDownloadService, + useClass: DesktopFileDownloadService, + deps: [], + }), + safeProvider({ + provide: SYSTEM_THEME_OBSERVABLE, + useFactory: () => fromIpcSystemTheme(), + deps: [], + }), + safeProvider({ + provide: EncryptedMessageHandlerService, + deps: [ + StateServiceAbstraction, + AuthServiceAbstraction, + CipherServiceAbstraction, + PolicyServiceAbstraction, + MessagingServiceAbstraction, + PasswordGenerationServiceAbstraction, + ], + }), + safeProvider({ + provide: NativeMessageHandlerService, + deps: [ + StateServiceAbstraction, + CryptoServiceAbstraction, + CryptoFunctionServiceAbstraction, + MessagingServiceAbstraction, + EncryptedMessageHandlerService, + DialogService, + DesktopAutofillSettingsService, + ], + }), + safeProvider({ + provide: CryptoFunctionServiceAbstraction, + useClass: RendererCryptoFunctionService, + deps: [WINDOW], + }), + safeProvider({ + provide: CryptoServiceAbstraction, + useClass: ElectronCryptoService, + deps: [ + InternalMasterPasswordServiceAbstraction, + KeyGenerationServiceAbstraction, + CryptoFunctionServiceAbstraction, + EncryptService, + PlatformUtilsServiceAbstraction, + LogService, + StateServiceAbstraction, + AccountServiceAbstraction, + StateProvider, + BiometricStateService, + ], + }), + safeProvider({ + provide: DesktopSettingsService, + deps: [StateProvider], + }), + safeProvider({ + provide: DesktopAutofillSettingsService, + deps: [StateProvider], + }), +]; @NgModule({ imports: [JslibServicesModule], declarations: [], - providers: [ - InitService, - NativeMessagingService, - SearchBarService, - LoginGuard, - DialogService, - { - provide: APP_INITIALIZER, - useFactory: (initService: InitService) => initService.init(), - deps: [InitService], - multi: true, - }, - { - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }, - { - provide: RELOAD_CALLBACK, - useValue: null, - }, - { provide: LogServiceAbstraction, useClass: ElectronLogRendererService, deps: [] }, - { - provide: PlatformUtilsServiceAbstraction, - useClass: ElectronPlatformUtilsService, - deps: [I18nServiceAbstraction, MessagingServiceAbstraction], - }, - { - // We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid - // the TokenService having to inject the PlatformUtilsService which introduces a - // circular dependency on Desktop only. - provide: SUPPORTS_SECURE_STORAGE, - useValue: ELECTRON_SUPPORTS_SECURE_STORAGE, - }, - { - provide: I18nServiceAbstraction, - useClass: I18nRendererService, - deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], - }, - { - provide: MessagingServiceAbstraction, - useClass: ElectronRendererMessagingService, - deps: [BroadcasterServiceAbstraction], - }, - { provide: AbstractStorageService, useClass: ElectronRendererStorageService }, - { provide: SECURE_STORAGE, useClass: ElectronRendererSecureStorageService }, - { provide: MEMORY_STORAGE, useClass: MemoryStorageService }, - { provide: OBSERVABLE_MEMORY_STORAGE, useClass: MemoryStorageServiceForStateProviders }, - { provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService }, - { - provide: SystemServiceAbstraction, - useClass: SystemService, - deps: [ - MessagingServiceAbstraction, - PlatformUtilsServiceAbstraction, - RELOAD_CALLBACK, - StateServiceAbstraction, - AutofillSettingsServiceAbstraction, - VaultTimeoutSettingsService, - BiometricStateService, - ], - }, - { - provide: StateServiceAbstraction, - useClass: ElectronStateService, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - STATE_FACTORY, - AccountServiceAbstraction, - EnvironmentService, - TokenService, - MigrationRunner, - STATE_SERVICE_USE_CACHE, - ], - }, - { - provide: FileDownloadService, - useClass: DesktopFileDownloadService, - }, - { - provide: SYSTEM_THEME_OBSERVABLE, - useFactory: () => fromIpcSystemTheme(), - }, - { - provide: EncryptedMessageHandlerService, - deps: [ - StateServiceAbstraction, - AuthServiceAbstraction, - CipherServiceAbstraction, - PolicyServiceAbstraction, - MessagingServiceAbstraction, - PasswordGenerationServiceAbstraction, - ], - }, - { - provide: NativeMessageHandlerService, - deps: [ - StateServiceAbstraction, - CryptoServiceAbstraction, - CryptoFunctionServiceAbstraction, - MessagingServiceAbstraction, - EncryptedMessageHandlerService, - DialogService, - ], - }, - { - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], - }, - { - provide: CryptoFunctionServiceAbstraction, - useClass: RendererCryptoFunctionService, - deps: [WINDOW], - }, - { - provide: CryptoServiceAbstraction, - useClass: ElectronCryptoService, - deps: [ - KeyGenerationServiceAbstraction, - CryptoFunctionServiceAbstraction, - EncryptService, - PlatformUtilsServiceAbstraction, - LogService, - StateServiceAbstraction, - AccountServiceAbstraction, - StateProvider, - BiometricStateService, - ], - }, - { - provide: DesktopSettingsService, - useClass: DesktopSettingsService, - deps: [StateProvider], - }, - ], + // Do not register your dependency here! Add it to the typesafeProviders array using the helper function + providers: safeProviders, }) export class ServicesModule {} diff --git a/apps/desktop/src/app/tools/export/export.component.ts b/apps/desktop/src/app/tools/export/export.component.ts index 3a740122eb..80ae3c80f9 100644 --- a/apps/desktop/src/app/tools/export/export.component.ts +++ b/apps/desktop/src/app/tools/export/export.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; -import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -12,6 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui"; @Component({ selector: "app-export", diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts index 53f919a596..51b5bf93a2 100644 --- a/apps/desktop/src/app/tools/generator.component.spec.ts +++ b/apps/desktop/src/app/tools/generator.component.spec.ts @@ -10,6 +10,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { GeneratorComponent } from "./generator.component"; @@ -54,6 +55,10 @@ describe("GeneratorComponent", () => { provide: LogService, useValue: mock(), }, + { + provide: CipherService, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/delete-account.component.html b/apps/desktop/src/auth/delete-account.component.html index 8639b0f5be..42c06b7489 100644 --- a/apps/desktop/src/auth/delete-account.component.html +++ b/apps/desktop/src/auth/delete-account.component.html @@ -7,7 +7,7 @@ {{ "deleteAccountWarning" | i18n }} -
+
{ let broadcasterServiceMock: MockProxy; let platformUtilsServiceMock: MockProxy; let activatedRouteMock: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; + + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); beforeEach(async () => { stateServiceMock = mock(); @@ -60,6 +70,8 @@ describe("LockComponent", () => { activatedRouteMock = mock(); activatedRouteMock.queryParams = mock(); + mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -67,6 +79,7 @@ describe("LockComponent", () => { await TestBed.configureTestingModule({ declarations: [LockComponent, I18nPipe], providers: [ + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, { provide: I18nService, useValue: mock(), @@ -147,6 +160,10 @@ describe("LockComponent", () => { provide: BiometricStateService, useValue: biometricStateService, }, + { + provide: AccountService, + useValue: accountService, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 7403f7481d..16b58c5bbe 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -9,7 +9,9 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -37,6 +39,7 @@ export class LockComponent extends BaseLockComponent { private autoPromptBiometric = false; constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, @@ -59,8 +62,10 @@ export class LockComponent extends BaseLockComponent { userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, + accountService: AccountService, ) { super( + masterPasswordService, router, i18nService, platformUtilsService, @@ -81,6 +86,7 @@ export class LockComponent extends BaseLockComponent { userVerificationService, pinCryptoService, biometricStateService, + accountService, ); } diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index b4242c36fb..0a339030ba 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -1,5 +1,5 @@ import { Location } from "@angular/common"; -import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { Router } from "@angular/router"; import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; @@ -7,12 +7,13 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -31,10 +32,7 @@ import { EnvironmentComponent } from "../environment.component"; selector: "app-login-via-auth-request", templateUrl: "login-via-auth-request.component.html", }) -export class LoginViaAuthRequestComponent - extends BaseLoginWithDeviceComponent - implements OnInit, OnDestroy -{ +export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { @ViewChild("environment", { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; showingModal = false; @@ -56,10 +54,11 @@ export class LoginViaAuthRequestComponent private modalService: ModalService, syncService: SyncService, stateService: StateService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, + accountService: AccountService, private location: Location, ) { super( @@ -77,10 +76,11 @@ export class LoginViaAuthRequestComponent anonymousHubService, validationService, stateService, - loginService, + loginEmailService, deviceTrustCryptoService, authRequestService, loginStrategyService, + accountService, ); super.onSuccessfulLogin = () => { @@ -109,10 +109,6 @@ export class LoginViaAuthRequestComponent }); } - ngOnDestroy(): void { - super.ngOnDestroy(); - } - back() { this.location.back(); } diff --git a/apps/desktop/src/auth/login/login.component.html b/apps/desktop/src/auth/login/login.component.html index 06ee5db32d..eef0580d4e 100644 --- a/apps/desktop/src/auth/login/login.component.html +++ b/apps/desktop/src/auth/login/login.component.html @@ -99,7 +99,7 @@ class="btn block" type="button" routerLink="/accessibility-cookie" - (click)="setFormValues()" + (click)="setLoginEmailValues()" > {{ "loadAccessibilityCookie" | i18n }} @@ -139,7 +139,7 @@ type="button" class="text text-primary password-hint-btn" routerLink="/hint" - (click)="setFormValues()" + (click)="setLoginEmailValues()" > {{ "getMasterPasswordHint" | i18n }} diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index dd22a0fa37..a810a29a26 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -3,13 +3,14 @@ import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; -import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, +} from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -35,8 +36,6 @@ const BroadcasterSubscriptionId = "LoginComponent"; export class LoginComponent extends BaseLoginComponent implements OnDestroy { @ViewChild("environment", { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; - @ViewChild("environmentSelector", { read: ViewContainerRef, static: true }) - environmentSelector: EnvironmentSelectorComponent; protected componentDestroyed$: Subject = new Subject(); webVaultHostname = ""; @@ -69,7 +68,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { formBuilder: FormBuilder, formValidationErrorService: FormValidationErrorsService, route: ActivatedRoute, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -89,7 +88,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { formBuilder, formValidationErrorService, route, - loginService, + loginEmailService, ssoLoginService, webAuthnLoginService, ); diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index a75668a856..93dfe0abd8 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -8,6 +8,8 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -29,6 +31,8 @@ const BroadcasterSubscriptionId = "SetPasswordComponent"; }) export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy { constructor( + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, apiService: ApiService, i18nService: I18nService, cryptoService: CryptoService, @@ -50,6 +54,8 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On dialogService: DialogService, ) { super( + accountService, + masterPasswordService, i18nService, cryptoService, messagingService, diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 0268133192..cc261f1235 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -7,8 +7,10 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -38,7 +40,9 @@ export class SsoComponent extends BaseSsoComponent { passwordGenerationService: PasswordGenerationServiceAbstraction, logService: LogService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, ) { super( ssoLoginService, @@ -55,6 +59,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index 9b862e7c9f..d1b84c1fa0 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -7,16 +7,18 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -56,10 +58,12 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -75,10 +79,12 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/autofill/services/desktop-autofill-settings.service.ts b/apps/desktop/src/autofill/services/desktop-autofill-settings.service.ts new file mode 100644 index 0000000000..d60a08a9fe --- /dev/null +++ b/apps/desktop/src/autofill/services/desktop-autofill-settings.service.ts @@ -0,0 +1,29 @@ +import { map } from "rxjs"; + +import { + AUTOFILL_SETTINGS_DISK, + KeyDefinition, + StateProvider, +} from "@bitwarden/common/platform/state"; + +const ENABLE_DUCK_DUCK_GO_BROWSER_INTEGRATION = new KeyDefinition( + AUTOFILL_SETTINGS_DISK, + "enableDuckDuckGoBrowserIntegration", + { + deserializer: (v: boolean) => v, + }, +); + +export class DesktopAutofillSettingsService { + private enableDuckDuckGoBrowserIntegrationState = this.stateProvider.getGlobal( + ENABLE_DUCK_DUCK_GO_BROWSER_INTEGRATION, + ); + readonly enableDuckDuckGoBrowserIntegration$ = + this.enableDuckDuckGoBrowserIntegrationState.state$.pipe(map((x) => x ?? false)); + + constructor(private stateProvider: StateProvider) {} + + async setEnableDuckDuckGoBrowserIntegration(newValue: boolean): Promise { + await this.enableDuckDuckGoBrowserIntegrationState.update(() => newValue); + } +} diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 1235230dd3..ee32c045c9 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index d7c405159f..104a9f7780 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 18f150745b..f404c7f95a 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Problemlərin aradan qaldırılması" + }, + "disableHardwareAccelerationRestart": { + "message": "Avadanlıq sürətləndirməni ləğv et və yenidən başlat" + }, + "enableHardwareAccelerationRestart": { + "message": "Avadanlıq sürətləndirməni işə sal və yenidən başlat" + }, + "removePasskey": { + "message": "Parolu sil" + }, + "passkeyRemoved": { + "message": "Parol silindi" } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 675f9cd0f2..2bc33f2b28 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 030dc45683..9f6d5bdd36 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Често използвани формати", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Отстраняване на проблеми" + }, + "disableHardwareAccelerationRestart": { + "message": "Изключете хардуерното ускорение и рестартирайте" + }, + "enableHardwareAccelerationRestart": { + "message": "Включете хардуерното ускорение и рестартирайте" + }, + "removePasskey": { + "message": "Премахване на секретния ключ" + }, + "passkeyRemoved": { + "message": "Секретният ключ е премахнат" } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index debefc0e65..22893aadab 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 4c6f2b8744..6adb5bbce3 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index f4ef70c2eb..c76111b53c 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -1645,10 +1645,10 @@ "message": "Habilita una capa de seguretat addicional mitjançant la validació de frases d'empremtes dactilars quan estableix un enllaç entre l'escriptori i el navegador. Quan està habilitat, requereix la intervenció i verificació de l'usuari cada vegada que s'estableix una connexió." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Utilitza l'acceleració de maquinari" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Per defecte, aquesta configuració està activada. Apagueu només si teniu problemes gràfics. Cal reiniciar." }, "approve": { "message": "Aprova" @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Formats comuns", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Resolució de problemes" + }, + "disableHardwareAccelerationRestart": { + "message": "Desactiveu l'acceleració de maquinari i reinicieu" + }, + "enableHardwareAccelerationRestart": { + "message": "Activeu l'acceleració i reinicieu el maquinari" + }, + "removePasskey": { + "message": "Suprimeix la clau de pas" + }, + "passkeyRemoved": { + "message": "Clau de pas suprimida" } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 534438aff2..e7ba56e81c 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Společné formáty", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Řešení problémů" + }, + "disableHardwareAccelerationRestart": { + "message": "Zakázat hardwarovou akceleraci a restartovat" + }, + "enableHardwareAccelerationRestart": { + "message": "Povolit hardwarovou akceleraci a restartovat" + }, + "removePasskey": { + "message": "Odebrat přístupový klíč" + }, + "passkeyRemoved": { + "message": "Přístupový klíč byl odebrán" } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index e266886a90..e87b805d0b 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 795c1cc272..1f994cf8eb 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -545,7 +545,7 @@ "message": "Angivelse af hovedadgangskode igen er obligatorisk." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Hovedadgangskoden skal udgøre minimum $VALUE$ tegn.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -780,7 +780,7 @@ "message": "Kontakt os" }, "helpAndFeedback": { - "message": "Help and feedback" + "message": "Hjælp og feedback" }, "getHelp": { "message": "Få hjælp" @@ -1420,10 +1420,10 @@ "message": "oplås din boks" }, "autoPromptWindowsHello": { - "message": "Bed om Windows Hello ved start" + "message": "Anmod om Windows Hello ved app-start" }, "autoPromptTouchId": { - "message": "Bed om Touch ID ved start" + "message": "Anmod om Touch ID ved app-start" }, "requirePasswordOnStart": { "message": "Kræv adgangskode eller PIN-kode ved app-start" @@ -1645,10 +1645,10 @@ "message": "Tilføj et ekstra sikkerhedslag ved at kræve bekræftelse af fingeraftrykssætning, når der oprettes forbindelse mellem din computer og din browser. Dette kræver brugerhandling og bekræftelse, hver gang der forbindelse oprettes." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Benyt hardwareacceleration" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Denne indstilling er som standard TIL. Slå kun FRA, hvis der opleves grafiske problemer. Genstart kræves." }, "approve": { "message": "Godkend" @@ -1931,7 +1931,7 @@ "message": "Minutter" }, "vaultTimeoutPolicyInEffect": { - "message": "Organisationspolitikker påvirker din boks-timeout. Maksimalt tilladt boks-timeout er $HOURS$ time(r) og $MINUTES$ minut(ter)", + "message": "Organisationspolitikkerne har fastsat den maksimalt tilladte boks-timeout til $HOURS$ time(r) og $MINUTES$ minut(ter).", "placeholders": { "hours": { "content": "$1", @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Almindelige formater", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Fejlfinding" + }, + "disableHardwareAccelerationRestart": { + "message": "Deaktivér hardwareacceleration og genstart" + }, + "enableHardwareAccelerationRestart": { + "message": "Aktivér hardwareacceleration og genstart" + }, + "removePasskey": { + "message": "Fjern adgangsnøgle" + }, + "passkeyRemoved": { + "message": "Adgangsnøgle fjernet" } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 4132caa7ad..428cfd6a27 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Gängigste Formate", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Problembehandlung" + }, + "disableHardwareAccelerationRestart": { + "message": "Hardwarebeschleunigung deaktivieren und neu starten" + }, + "enableHardwareAccelerationRestart": { + "message": "Hardwarebeschleunigung aktivieren und neu starten" + }, + "removePasskey": { + "message": "Passkey entfernen" + }, + "passkeyRemoved": { + "message": "Passkey entfernt" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 28e2f9f870..63b1f21c2e 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -404,7 +404,7 @@ "message": "Μήκος" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Ελάχιστο μήκος κωδικού" }, "uppercase": { "message": "Κεφαλαία (A-Z)" @@ -479,7 +479,7 @@ "message": "Το μέγιστο μέγεθος αρχείου είναι 500 MB." }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "Απαιτείται μεταφορά κλειδιού κρυπτογράφησης. Παρακαλούμε συνδεθείτε μέσω του διαδικτυακής κρύπτης για να ενημερώσετε το κλειδί κρυπτογράφησης." }, "editedFolder": { "message": "Ο φάκελος αποθηκεύτηκε" @@ -561,10 +561,10 @@ "message": "Ο λογαριασμός σας έχει δημιουργηθεί! Τώρα μπορείτε να συνδεθείτε." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Έχετε συνδεθεί επιτυχώς" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Μπορείτε να κλείσετε αυτό το παράθυρο" }, "masterPassSent": { "message": "Σας στείλαμε ένα email με την υπόδειξη του κύριου κωδικού." @@ -1087,7 +1087,7 @@ "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Ιδιόκτητες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey και το Duo." }, "premiumSignUpReports": { "message": "Ασφάλεια κωδικών, υγιής λογαριασμός και αναφορές παραβίασης δεδομένων για να διατηρήσετε ασφαλή τη λίστα σας." @@ -1399,7 +1399,7 @@ "message": "Μη έγκυρος κωδικός PIN." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Πάρα πολλές άκυρες απόπειρες εισαγωγής PIN. Γίνεται αποσύνδεση." }, "unlockWithWindowsHello": { "message": "Ξεκλειδώστε με το Windows Hello" @@ -1414,7 +1414,7 @@ "message": "Ξεκλείδωμα με Touch ID" }, "additionalTouchIdSettings": { - "message": "Additional Touch ID settings" + "message": "Πρόσθετες ρυθμίσεις Touch ID" }, "touchIdConsentMessage": { "message": "Ξεκλειδώστε το vault σας" @@ -1505,7 +1505,7 @@ "message": "Ένα αποσυνδεδεμένο vault απαιτεί να κάνετε ξανά έλεγχο ταυτότητας για να αποκτήσετε πρόσβαση σε αυτό." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου κρύπτης." }, "lock": { "message": "Κλείδωμα", @@ -1546,15 +1546,15 @@ "message": "Ορισμός Κύριου Κωδικού" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Τα δικαιώματα του οργανισμού σας ενημερώθηκαν, απαιτώντας από εσάς να ορίσετε έναν κύριο κωδικό πρόσβασης.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Ο οργανισμός σας απαιτεί να ορίσετε έναν κύριο κωδικό πρόσβασης.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Απαιτείται επαλήθευση", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1645,10 +1645,10 @@ "message": "Ενεργοποιήστε ένα πρόσθετο επίπεδο ασφάλειας απαιτώντας επικύρωση φράσης δακτυλικών αποτυπωμάτων κατά τη δημιουργία μιας σύνδεσης μεταξύ της επιφάνειας εργασίας σας και του προγράμματος περιήγησης. Όταν ενεργοποιηθεί, αυτό απαιτεί παρέμβαση χρήστη και επαλήθευση κάθε φορά που δημιουργείται σύνδεση." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Χρήση επιτάχυνσης υλικού" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Εξ ορισμού αυτή η ρύθμιση είναι ΕΝΕΡΓΗ. Απενεργοποιήστε μόνο αν αντιμετωπίζετε γραφικά προβλήματα. Απαιτείται επανεκκίνηση." }, "approve": { "message": "Έγκριση" @@ -1690,7 +1690,7 @@ "message": "Μια πολιτική του οργανισμού, επηρεάζει τις επιλογές ιδιοκτησίας σας." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Μια πολιτική οργανισμού έχει αποτρέψει την εισαγωγή στοιχείων στην προσωπική κρύπτη σας." }, "allSends": { "message": "Όλα τα Sends", @@ -1886,43 +1886,43 @@ "message": "Ο Κύριος Κωδικός Πρόσβασής σας άλλαξε πρόσφατα από διαχειριστή στον οργανισμό σας. Για να αποκτήσετε πρόσβαση στο vault, πρέπει να τον ενημερώσετε τώρα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Ο κύριος κωδικός πρόσβασης δεν πληροί τις απαιτήσεις πολιτικής αυτού του οργανισμού. Για να έχετε πρόσβαση στην κρύπτη, πρέπει να ενημερώσετε τον κύριο κωδικό σας άμεσα. Η διαδικασία θα σάς αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για το πολύ μία ώρα." }, "tryAgain": { - "message": "Try again" + "message": "Προσπαθήστε ξανά" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Απαιτείται επαλήθευση για αυτήν την ενέργεια. Ορίστε ένα PIN για να συνεχίσετε." }, "setPin": { - "message": "Set PIN" + "message": "Ορισμός PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Επαλήθευση με βιομετρικά" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Σε αναμονή επιβεβαίωσης" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Αδύνατη η ολοκλήρωση των βιομετρικών." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Χρειάζεστε μια διαφορετική μέθοδο;" }, "useMasterPassword": { - "message": "Use master password" + "message": "Χρήση κύριου κωδικού" }, "usePin": { - "message": "Use PIN" + "message": "Χρήση PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Χρήση βιομετρικών" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Εισάγετε τον κωδικό επαλήθευσης που έχει σταλεί στο email σας." }, "resendCode": { - "message": "Resend code" + "message": "Επαναποστολή κωδικού" }, "hours": { "message": "Ώρες" @@ -1944,7 +1944,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Οι πολιτικές του οργανισμού σας επηρεάζουν το χρονικό όριο λήξης της κρύ[της σας. Το μέγιστο επιτρεπόμενο χρονικό όριο λήξης vault είναι $HOURS$ ώρα(ες) και $MINUTES$ λεπτό(ά). H ενέργεια χρονικού ορίου λήξης της κρύπτης είναι ορισμένη ως $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -1961,7 +1961,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Οι πολιτικές του οργανισμού σας έχουν ορίσει την ενέργεια χρονικού ορίου λήξης κρύπτης σε $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2051,7 +2051,7 @@ "message": "Εξαγωγή Προσωπικού Vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Μόνο τα μεμονωμένα αντικείμενα κρύπτης που σχετίζονται με το $EMAIL$ θα εξαχθούν. Τα αντικείμενα κρύπτης οργανισμού δε θα συμπεριληφθούν. Μόνο πληροφορίες αντικειμένων κρύπτης θα εξαχθούν και δε θα περιλαμβάνουν συσχετιζόμενα συνημμένα.", "placeholders": { "email": { "content": "$1", @@ -2176,7 +2176,7 @@ "message": "Συνδεθείτε με άλλη συσκευή" }, "loginInitiated": { - "message": "Login initiated" + "message": "Η σύνδεση ξεκίνησε" }, "notificationSentDevice": { "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." @@ -2310,7 +2310,7 @@ } }, "windowsBiometricUpdateWarning": { - "message": "Bitwarden recommends updating your biometric settings to require your master password (or PIN) on the first unlock. Would you like to update your settings now?" + "message": "Το Bitwarden συστήνει την ενημέρωση των βιομετρικών ρυθμίσεών σας ώστε να απαιτηθεί ο κύριος κωδικός πρόσβασης (ή PIN) στο πρώτο ξεκλείδωμα. Θέλετε να ενημερώσετε τις ρυθμίσεις σας τώρα;" }, "windowsBiometricUpdateWarningTitle": { "message": "Ενημέρωση Προτεινόμενων Ρυθμίσεων" @@ -2319,74 +2319,74 @@ "message": "Απαιτείται έγκριση συσκευής. Επιλέξτε μια επιλογή έγκρισης παρακάτω:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Απομνημόνευση αυτής της συσκευής" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Αποεπιλογή αν γίνεται χρήση δημόσιας συσκευής" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Έγκριση από άλλη συσκευή σας" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Αίτηση έγκρισης διαχειριστή" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Έγκριση με τον κύριο κωδικό" }, "region": { - "message": "Region" + "message": "Περιοχή" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Απαιτείται αναγνωριστικό οργανισμού SSO." }, "eu": { - "message": "EU", + "message": "ΕΕ", "description": "European Union" }, "loggingInOn": { - "message": "Logging in on" + "message": "Σύνδεση σε" }, "selfHostedServer": { - "message": "self-hosted" + "message": "αυτο-φιλοξενούμενο" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "Δεν επιτρέπεται η πρόσβαση. Δεν έχετε άδεια για να δείτε αυτή τη σελίδα." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Επιτυχής δημιουργία λογαριασμού!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Ζητήθηκε έγκριση διαχειριστή" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Το αίτημά σας εστάλη στον διαχειριστή σας." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Θα ειδοποιηθείτε μόλις εγκριθεί." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Πρόβλημα σύνδεσης;" }, "loginApproved": { - "message": "Login approved" + "message": "Η σύνδεση εγκρίθηκε" }, "userEmailMissing": { - "message": "User email missing" + "message": "Το email του χρήστη λείπει" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Αξιόπιστη συσκευή" }, "inputRequired": { - "message": "Input is required." + "message": "Απαιτείται είσοδος." }, "required": { "message": "απαιτείται" }, "search": { - "message": "Search" + "message": "Αναζήτηση" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Η είσοδος πρέπει να είναι τουλάχιστον $COUNT$ χαρακτήρες.", "placeholders": { "count": { "content": "$1", @@ -2395,7 +2395,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Η είσοδος δεν πρέπει να υπερβαίνει τους $COUNT$ χαρακτήρες.", "placeholders": { "count": { "content": "$1", @@ -2404,7 +2404,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Οι ακόλουθοι χαρακτήρες δεν επιτρέπονται: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2413,7 +2413,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Η τιμή εισόδου πρέπει να είναι τουλάχιστον $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2422,7 +2422,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Η τιμή εισόδου δεν πρέπει να υπερβαίνει το $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2431,17 +2431,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 ή περισσότερα email δεν είναι έγκυρα" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Η είσοδος δεν πρέπει να περιέχει μόνο κενά.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Η είσοδος δεν είναι διεύθυνση email." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ πεδίο(α) παραπάνω χρειάζονται την προσοχή σας.", "placeholders": { "count": { "content": "$1", @@ -2450,22 +2450,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Επιλογή --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Πληκτρολογήστε για φιλτράρισμα --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Ανάκτηση επιλογών..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Δεν βρέθηκαν αντικείμενα" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Εκκαθάριση όλων" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ ακόμα", "placeholders": { "quantity": { "content": "$1", @@ -2474,44 +2474,44 @@ } }, "submenu": { - "message": "Submenu" + "message": "Υπομενού" }, "skipToContent": { - "message": "Skip to content" + "message": "Μετάβαση στο περιεχόμενο" }, "typePasskey": { - "message": "Passkey" + "message": "Κλειδί πρόσβασης" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Το κλειδί πρόσβασης δεν θα αντιγραφεί" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί στο κλωνοποιημένο στοιχείο. Θέλετε να συνεχίσετε την κλωνοποίηση αυτού του στοιχείου;" }, "aliasDomain": { - "message": "Alias domain" + "message": "Ψευδώνυμο τομέα" }, "importData": { - "message": "Import data", + "message": "Εισαγωγή δεδομένων", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "Import error" + "message": "Σφάλμα κατά την εισαγωγή" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Παρουσιάστηκε πρόβλημα με τα δεδομένα που επιχειρήσατε να εισαγάγετε. Παρακαλώ επιλύστε τα σφάλματα που αναφέρονται παρακάτω στο αρχείο πηγής και προσπαθήστε ξανά." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Επιλύστε τα παρακάτω σφάλματα και προσπαθήστε ξανά." }, "description": { - "message": "Description" + "message": "Περιγραφή" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Τα δεδομένα εισήχθησαν επιτυχώς" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Ένα σύνολο $AMOUNT$ στοιχείων εισήχθησαν.", "placeholders": { "amount": { "content": "$1", @@ -2520,10 +2520,10 @@ } }, "total": { - "message": "Total" + "message": "Σύνολο" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Εισαγάγετε δεδομένα στην $ORGANIZATION$. Τα δεδομένα σας μπορεί να μοιραστούν με μέλη αυτού του οργανισμού. Θέλετε να συνεχίσετε;", "placeholders": { "organization": { "content": "$1", @@ -2532,40 +2532,40 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Εκκινήστε το Duo και ακολουθήστε τα βήματα για να ολοκληρώσετε τη σύνδεση." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Η Σύνδεση δύο βημάτων Duo απαιτείται για τον λογαριασμό σας." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Εκκίνηση Duo στον περιηγητή" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Τα δεδομένα δεν έχουν διαμορφωθεί σωστά. Ελέγξτε το αρχείο εισαγωγής και δοκιμάστε ξανά." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Τίποτα δεν εισήχθη." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Σφάλμα αποκρυπτογράφησης του εξαγόμενου αρχείου. Το κλειδί κρυπτογράφησης δεν ταιριάζει με το κλειδί κρυπτογράφησης που χρησιμοποιήθηκε για την εξαγωγή των δεδομένων." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Μη έγκυρος κωδικός πρόσβασης, παρακαλώ χρησιμοποιήστε τον κωδικό πρόσβασης που εισαγάγατε όταν δημιουργήσατε το αρχείο εξαγωγής." }, "importDestination": { - "message": "Import destination" + "message": "Προορισμός εισαγωγής" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Μάθετε για τις επιλογές εισαγωγής σας" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Επιλέξτε ένα φάκελο" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Επιλέξτε μια συλλογή" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Επιλέξτε αυτό αν θέλετε τα περιεχόμενα του εισαγόμενου αρχείου να μετακινηθούν σε $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2575,25 +2575,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Το αρχείο περιέχει μη συσχετισμένα στοιχεία." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Επιλέξτε τη μορφή του αρχείου εισαγωγής" }, "selectImportFile": { - "message": "Select the import file" + "message": "Επιλέξτε το αρχείο εισαγωγής" }, "chooseFile": { - "message": "Choose File" + "message": "Επιλογή Αρχείου" }, "noFileChosen": { - "message": "No file chosen" + "message": "Δεν επιλέχθηκε κανένα αρχείο" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "ή αντιγράψτε/επικολλήστε τα περιεχόμενα του αρχείου εισαγωγής" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ Οδηγίες", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2603,89 +2603,104 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Επιβεβαίωση εισαγωγής κρύπτης" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Αυτό το αρχείο προστατεύεται με κωδικό πρόσβασης. Παρακαλώ εισαγάγετε τον κωδικό πρόσβασης για την εισαγωγή δεδομένων." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Επιβεβαίωση κωδικού πρόσβασης αρχείου" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Ο πολυμερής έλεγχος ταυτότητας ακυρώθηκε" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Δεν βρέθηκαν δεδομένα LastPass" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Λάθος όνομα χρήστη ή κωδικού πρόσβασης" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Λάθος κωδικός" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Λάθος κωδικός" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Λάθος PIN" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Ο πολυμερής έλεγχος ταυτότητας απέτυχε" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Συμπερίληψη κοινόχρηστων φακέλων" }, "lastPassEmail": { "message": "LastPass Email" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Εισαγωγή του λογαριασμού σας..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Απαιτείται πολυμερής ταυτοποίηση LastPass" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Εισαγάγετε τον κωδικό μιας χρήσης από την εφαρμογή επαλήθευσης" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Εγκρίνετε το αίτημα σύνδεσης στην εφαρμογή επαλήθευσης ή εισαγάγετε έναν κωδικό πρόσβασης μιας χρήσης." }, "passcode": { - "message": "Passcode" + "message": "Κωδικός" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "Κύριος κωδικός πρόσβασης LastPass" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Απαιτείται ταυτοποίηση LastPass" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Αναμονή ελέγχου ταυτότητας SSO" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Παρακαλούμε συνεχίστε τη σύνδεση χρησιμοποιώντας τα στοιχεία της εταιρείας σας." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Δείτε λεπτομερείς οδηγίες στην ιστοσελίδα βοήθειας μας στο", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Εισαγωγή απευθείας από το LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Εισαγωγή από CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Δοκιμάστε ξανά ή ψάξτε για ένα email από το LastPass για να επιβεβαιώσετε ότι είστε εσείς." }, "collection": { - "message": "Collection" + "message": "Συλλογή" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Εισαγάγετε το YubiKey που σχετίζεται με το λογαριασμό LastPass στη θύρα USB του υπολογιστή σας και στη συνέχεια αγγίξτε το κουμπί του." }, "commonImportFormats": { - "message": "Common formats", + "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Αντιμετώπιση Προβλημάτων" + }, + "disableHardwareAccelerationRestart": { + "message": "Απενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" + }, + "enableHardwareAccelerationRestart": { + "message": "Ενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 8a6233ec63..00eace54e2 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -2687,5 +2690,26 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 25f6b61758..53958bca57 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 680586cd71..f6011c301f 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 9327dceae4..772eb70985 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index f30239ad75..e3dcd0dc4c 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index c2780c3c97..2b54df2a91 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index b3426a5146..d66d5265e1 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 5e7b26fbb4..c62bb99b2d 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index b04b859d35..f74136aedc 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -494,7 +494,7 @@ "message": "Kansio poistettiin" }, "loginOrCreateNewAccount": { - "message": "Käytä salattua holviasi kirjautumalla sisään tai tai luo uusi tili." + "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." }, "createAccount": { "message": "Luo tili" @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Yleiset muodot", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Vianetsintä" + }, + "disableHardwareAccelerationRestart": { + "message": "Poista laitteistokiihdytys käytöstä ja käynnistä sovellus uudelleen" + }, + "enableHardwareAccelerationRestart": { + "message": "Ota laitteistokiihdytys käyttöön ja käynnistä sovellus uudelleen" + }, + "removePasskey": { + "message": "Poista suojausavain" + }, + "passkeyRemoved": { + "message": "Suojausavain poistettiin" } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 41cd3e9bce..170559fc64 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index edecc8a4d6..20353d2d86 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -448,7 +448,7 @@ "message": "Rechercher dans la collection" }, "searchFolder": { - "message": "Rechercher dans un dossier" + "message": "Rechercher dans le dossier" }, "searchFavorites": { "message": "Rechercher parmi les favoris" @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Formats communs", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Résolution de problèmes" + }, + "disableHardwareAccelerationRestart": { + "message": "Désactiver l'accélération matérielle et redémarrer" + }, + "enableHardwareAccelerationRestart": { + "message": "Activer l'accélération matérielle et redémarrer" + }, + "removePasskey": { + "message": "Retirer la clé d'identification (passkey)" + }, + "passkeyRemoved": { + "message": "Clé d'identification (passkey) retirée" } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index f5f85b040c..f96260c005 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 607c2d81d3..cc5a0f011d 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -561,10 +561,10 @@ "message": "החשבון החדש שלך נוצר בהצלחה! כעת ניתן להתחבר למערכת." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "נכנסת בהצלחה" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "אפשר לסגור את החלון הזה" }, "masterPassSent": { "message": "שלחנו לך אימייל עם רמז לסיסמה הראשית." @@ -1505,7 +1505,7 @@ "message": "יש לבצע אימות מחדש כדי לגשת לכספת שוב." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "יש להגדיר שיטת שחרור נעילה כדי לשנות את פעולת תום הזמן של הכספת שלך." }, "lock": { "message": "נעילה", @@ -1645,10 +1645,10 @@ "message": "הפעל שכבת אבטחה נוספת באמצעות בקשת אימות טביעות אצבע בעת יצירת קישור בין שולחן העבודה לדפדפן. כשהוא מופעל, זה דורש התערבות ואימות משתמש בכל פעם שנוצר חיבור." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "להשתמש בהאצת חומרה" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "ההגדרה הזאת פעילה כברירת מחדל. יש לכבות רק אם יש כל מיני תקלות גרפיות. צריך להפעיל מחדש." }, "approve": { "message": "לְאַשֵׁר" @@ -2295,7 +2295,7 @@ "message": "Check known data breaches for this password" }, "important": { - "message": "Important:" + "message": "חשוב:" }, "masterPasswordHint": { "message": "Your master password cannot be recovered if you forget it!" @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 4b5634baf1..57ef1d32ef 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 9f68d70a87..1e501cee78 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index f7e4052c17..0b443c9a6b 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Általános formátumok", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Hibaelhárítás" + }, + "disableHardwareAccelerationRestart": { + "message": "A hardveres gyorsítás letiltása és újraindítás" + }, + "enableHardwareAccelerationRestart": { + "message": "A hardveres gyorsítás engedélyezése és újraindítás" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 10a01a385c..008b6b369a 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 5c1c750c85..a3a6f771fe 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Formati comuni", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Risoluzione problemi" + }, + "disableHardwareAccelerationRestart": { + "message": "Disattiva l'accelerazione hardware e riavvia" + }, + "enableHardwareAccelerationRestart": { + "message": "Attiva l'accelerazione hardware e riavvia" + }, + "removePasskey": { + "message": "Rimuovi passkey" + }, + "passkeyRemoved": { + "message": "Passkey rimossa" } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 1a1a0e148b..f0a95d4e35 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "一般的な形式", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "トラブルシューティング" + }, + "disableHardwareAccelerationRestart": { + "message": "ハードウェアアクセラレーションを無効にして再起動する" + }, + "enableHardwareAccelerationRestart": { + "message": "ハードウェアアクセラレーションを有効にして再起動する" + }, + "removePasskey": { + "message": "パスキーを削除" + }, + "passkeyRemoved": { + "message": "パスキーを削除しました" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index f5f85b040c..f96260c005 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index f5f85b040c..f96260c005 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 1707f85a9b..281d64cbc2 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index cd22b12386..a09d53b1dd 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 6177164408..dbc2e13d1c 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Dažni formatai", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 502f926c78..0a3501dded 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -1645,10 +1645,10 @@ "message": "Iespējo papildu drošības slāni, pieprasot atpazīšanas vārdkopas pārbaudi, kad tiek izveidota saikne starp darbvirsmu un pārlūku. Ir nepieciešama lietotāja darbība un apliecināšana katru reizi, kad tiek izveidots savienojums." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Izmantot aparatūras paātrināšanu" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Pēc noklusējuma šis iestatījums ir ieslēgts. Izslēgt tikai tad, ja tiek pieredzētas attēlojuma nepilnības. Nepieciešama pārsāknēšana." }, "approve": { "message": "Apstiprināt" @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Sarežģījumu novēršana" + }, + "disableHardwareAccelerationRestart": { + "message": "Atspējot aparatūras paātrinājumu un pārsāknēt" + }, + "enableHardwareAccelerationRestart": { + "message": "Iespējot aparatūras paātrinājumu un pārsāknēt" + }, + "removePasskey": { + "message": "Noņemt piekļuves atslēgu" + }, + "passkeyRemoved": { + "message": "Piekļuves atslēga noņemta" } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 4aa0c6598a..d5e3bddf8e 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index c025e77c41..6b1137d232 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index f5f85b040c..f96260c005 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index dc9180ddbf..2626e93c24 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index cda18630e6..8e8e2e2cbb 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Vanlige formater", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 4eed6500a6..e7d586023e 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 188a4ab3cb..9c4ba78036 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Probleemoplossing" + }, + "disableHardwareAccelerationRestart": { + "message": "Hardwareversnelling uitschakelen en herstarten" + }, + "enableHardwareAccelerationRestart": { + "message": "Hardwareversnelling inschakelen en herstarten" + }, + "removePasskey": { + "message": "Passkey verwijderen" + }, + "passkeyRemoved": { + "message": "Passkey verwijderd" } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index d088661002..ea55378f5b 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index b67bed747c..c6c0c2fb0c 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 3a8d6dd4ef..a0626a6c90 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Popularne formaty", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Rozwiązywanie problemów" + }, + "disableHardwareAccelerationRestart": { + "message": "Wyłącz akcelerację sprzętową i uruchom ponownie" + }, + "enableHardwareAccelerationRestart": { + "message": "Włącz akcelerację sprzętową i uruchom ponownie" + }, + "removePasskey": { + "message": "Usuń passkey" + }, + "passkeyRemoved": { + "message": "Passkey został usunięty" } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index 8c29fc80d7..c8f8316e6d 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Formatos comuns", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 26635769ab..5fbd7636d1 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Formatos comuns", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Resolução de problemas" + }, + "disableHardwareAccelerationRestart": { + "message": "Desativar a aceleração de hardware e reiniciar" + }, + "enableHardwareAccelerationRestart": { + "message": "Ativar a aceleração de hardware e reiniciar" + }, + "removePasskey": { + "message": "Remover chave de acesso" + }, + "passkeyRemoved": { + "message": "Chave de acesso removida" } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index b6c0016e67..7af8f7ec16 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 817c707884..0e28c2cf90 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -2480,13 +2480,13 @@ "message": "Перейти к содержимому" }, "typePasskey": { - "message": "Ключ доступа" + "message": "Passkey" }, "passkeyNotCopied": { - "message": "Ключ доступа не будет скопирован" + "message": "Passkey не будет скопирован" }, "passkeyNotCopiedAlert": { - "message": "Ключ доступа не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" + "message": "Passkey не будет скопирован в клонированный элемент. Продолжить клонирование этого элемента?" }, "aliasDomain": { "message": "Псевдоним домена" @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Основные форматы", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Устранение проблем" + }, + "disableHardwareAccelerationRestart": { + "message": "Отключить аппаратное ускорение и перезапустить" + }, + "enableHardwareAccelerationRestart": { + "message": "Включить аппаратное ускорение и перезапустить" + }, + "removePasskey": { + "message": "Удалить passkey" + }, + "passkeyRemoved": { + "message": "Passkey удален" } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 92326f4312..60e8aea93c 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 0cfa6a98e0..240b883254 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Bežné formáty", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Riešenie problémov" + }, + "disableHardwareAccelerationRestart": { + "message": "Zakázať hardvérové zrýchlenie a reštartovať" + }, + "enableHardwareAccelerationRestart": { + "message": "Povoliť hardvérové zrýchlenie a reštartovať" + }, + "removePasskey": { + "message": "Odstrániť prístupový kľúč" + }, + "passkeyRemoved": { + "message": "Prístupový kľúč bol odstránený" } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index afdf7fc63a..528274cf29 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 37335b207c..77b5f7221d 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1645,10 +1645,10 @@ "message": "Омогућите додатни ниво заштите захтевајући проверу фразе отиска прста приликом успостављања везе између desktop-а и прегледача. Када је омогућено, ово захтева интервенцију и верификацију корисника сваки пут када се успостави веза." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Користи хардверско убрзање" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Подразумевано је ово подешавање УКЉУЧЕНО. ИСКЉУЧИТЕ само ако имате графичких проблема. Потребно је поновно покретање." }, "approve": { "message": "Одобри" @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Уобичајени формати", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Решавање проблема" + }, + "disableHardwareAccelerationRestart": { + "message": "Онемогућите хардверско убрзање и поново покрените" + }, + "enableHardwareAccelerationRestart": { + "message": "Омогућите хардверско убрзање и поново покрените" + }, + "removePasskey": { + "message": "Уклонити приступачни кључ" + }, + "passkeyRemoved": { + "message": "Приступачни кључ је уклоњен" } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 53fe6b58d2..c07f7efef3 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Vanliga format", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Felsökning" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Ta bort nyckel" + }, + "passkeyRemoved": { + "message": "Nyckel borttagen" } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index f5f85b040c..f96260c005 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 29b04ef6f2..efbfc86b33 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index db28be4e5e..36d62ed2a5 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Sorun giderme" + }, + "disableHardwareAccelerationRestart": { + "message": "Donanım hızlandırmayı devre dışı bırakın ve yeniden başlatın" + }, + "enableHardwareAccelerationRestart": { + "message": "Donanım hızlandırmayı etkinleştirin ve yeniden başlatın" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index e395c4409f..377fd23b0b 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Поширені формати", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Усунення проблем" + }, + "disableHardwareAccelerationRestart": { + "message": "Вимкнути апаратне прискорення і перезапустити" + }, + "enableHardwareAccelerationRestart": { + "message": "Увімкнути апаратне прискорення і перезапустити" + }, + "removePasskey": { + "message": "Вилучити ключ доступу" + }, + "passkeyRemoved": { + "message": "Ключ доступу вилучено" } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index b97eef4445..138773d40a 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -2687,5 +2687,20 @@ "commonImportFormats": { "message": "Common formats", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "Troubleshooting" + }, + "disableHardwareAccelerationRestart": { + "message": "Disable hardware acceleration and restart" + }, + "enableHardwareAccelerationRestart": { + "message": "Enable hardware acceleration and restart" + }, + "removePasskey": { + "message": "Remove passkey" + }, + "passkeyRemoved": { + "message": "Passkey removed" } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 4bf1530019..8725fa0f21 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -1907,7 +1907,7 @@ "message": "无法完成生物识别。" }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "使用其他方式?" }, "useMasterPassword": { "message": "使用主密码" @@ -2685,7 +2685,22 @@ "message": "将与您的 LastPass 账户关联的 YubiKey 插入计算机的 USB 端口,然后触摸其按钮。" }, "commonImportFormats": { - "message": "通用格式", + "message": "常规格式", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "故障排除" + }, + "disableHardwareAccelerationRestart": { + "message": "禁用硬件加速并重启" + }, + "enableHardwareAccelerationRestart": { + "message": "启用硬件加速并重启" + }, + "removePasskey": { + "message": "移除通行密钥" + }, + "passkeyRemoved": { + "message": "通行密钥已移除" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index a4d280def7..c93f236976 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -404,7 +404,7 @@ "message": "長度" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "最小密碼長度" }, "uppercase": { "message": "大寫 (A-Z)" @@ -561,10 +561,10 @@ "message": "帳戶已建立!現在可以登入了。" }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "你已成功登入" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "你可以關閉此視窗" }, "masterPassSent": { "message": "已寄出包含您主密碼提示的電子郵件。" @@ -1546,15 +1546,15 @@ "message": "設定主密碼" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "你的組織權限已更新,要求你設定一個主密碼。", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "你的組織要求你設定一個主密碼。", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "需要驗證", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1645,10 +1645,10 @@ "message": "在您的桌面和瀏覽器閒建立連綫時,透過要求指紋短語確認,以添加一個額外的安全層。每次建立連綫都需要使用者干預和驗證。" }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "使用硬體加速" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "此設定預設為開啟。僅當你遇到圖形問題時才關閉。需要重新啟動。" }, "approve": { "message": "核准" @@ -1889,40 +1889,40 @@ "message": "您的主密碼不符合一個或多個組織原則要求。您必須立即更新您的主密碼才能存取密碼庫。進行此動作將登出您目前的工作階段,需要您重新登入。其他裝置上的工作階段可能繼續長達一小時。" }, "tryAgain": { - "message": "Try again" + "message": "再試一次" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "此操作需要驗證。設定 PIN 碼以繼續。" }, "setPin": { - "message": "Set PIN" + "message": "設定 PIN 碼" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "使用生物辨識進行驗證" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "正在等待確認" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "無法完成生物辨識。" }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "需要不同的方法嗎?" }, "useMasterPassword": { - "message": "Use master password" + "message": "使用主密碼" }, "usePin": { - "message": "Use PIN" + "message": "使用 PIN 碼" }, "useBiometrics": { - "message": "Use biometrics" + "message": "使用生物辨識" }, "enterVerificationCodeSentToEmail": { "message": "Enter the verification code that was sent to your email." }, "resendCode": { - "message": "Resend code" + "message": "重新傳送驗證碼" }, "hours": { "message": "小時" @@ -2465,7 +2465,7 @@ "message": "全部清除" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ 個更多", "placeholders": { "quantity": { "content": "$1", @@ -2477,7 +2477,7 @@ "message": "子選單" }, "skipToContent": { - "message": "Skip to content" + "message": "跳至內容" }, "typePasskey": { "message": "密碼金鑰" @@ -2621,13 +2621,13 @@ "message": "使用者名稱或密碼不正確" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "密碼不正確" }, "incorrectCode": { - "message": "Incorrect code" + "message": "驗證碼不正確" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN 碼不正確" }, "multifactorAuthenticationFailed": { "message": "多因素驗證失敗" @@ -2685,7 +2685,22 @@ "message": "將與您的 LastPass 帳戶關聯的 YubiKey 插入電腦的 USB 連接埠,然後觸摸其按鈕。" }, "commonImportFormats": { - "message": "Common formats", + "message": "常見格式", "description": "Label indicating the most common import formats" + }, + "troubleshooting": { + "message": "疑難排解" + }, + "disableHardwareAccelerationRestart": { + "message": "停用硬體加速並重新啟動" + }, + "enableHardwareAccelerationRestart": { + "message": "啟用硬體加速並重新啟動" + }, + "removePasskey": { + "message": "移除金鑰" + }, + "passkeyRemoved": { + "message": "金鑰已移除" } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index ee36d49dd9..67f08839c5 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -6,11 +6,15 @@ import { firstValueFrom } from "rxjs"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -26,6 +30,7 @@ import { StateEventRegistrarService } from "@bitwarden/common/platform/state/sta import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; /* eslint-enable import/no-restricted-paths */ +import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service"; import { MenuMain } from "./main/menu/menu.main"; import { MessagingMain } from "./main/messaging.main"; import { NativeMessagingMain } from "./main/native-messaging.main"; @@ -44,7 +49,9 @@ import { ELECTRON_SUPPORTS_SECURE_STORAGE } from "./platform/services/electron-p import { ElectronStateService } from "./platform/services/electron-state.service"; import { ElectronStorageService } from "./platform/services/electron-storage.service"; import { I18nMainService } from "./platform/services/i18n.main.service"; +import { IllegalSecureStorageService } from "./platform/services/illegal-secure-storage.service"; import { ElectronMainMessagingService } from "./services/electron-main-messaging.service"; +import { isMacAppStore } from "./utils"; export class Main { logService: ElectronLogMainService; @@ -60,6 +67,8 @@ export class Main { desktopSettingsService: DesktopSettingsService; migrationRunner: MigrationRunner; tokenService: TokenServiceAbstraction; + keyGenerationService: KeyGenerationServiceAbstraction; + encryptService: EncryptService; windowMain: WindowMain; messagingMain: MessagingMain; @@ -70,6 +79,7 @@ export class Main { biometricsService: BiometricsServiceAbstraction; nativeMessagingMain: NativeMessagingMain; clipboardMain: ClipboardMain; + desktopAutofillSettingsService: DesktopAutofillSettingsService; constructor() { // Set paths for portable builds @@ -150,11 +160,28 @@ export class Main { this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); + this.mainCryptoFunctionService = new MainCryptoFunctionService(); + this.mainCryptoFunctionService.init(); + + this.keyGenerationService = new KeyGenerationService(this.mainCryptoFunctionService); + + this.encryptService = new EncryptServiceImplementation( + this.mainCryptoFunctionService, + this.logService, + true, // log mac failures + ); + + // Note: secure storage service is not available and should not be called in the main background process. + const illegalSecureStorageService = new IllegalSecureStorageService(); + this.tokenService = new TokenService( singleUserStateProvider, globalStateProvider, ELECTRON_SUPPORTS_SECURE_STORAGE, - this.storageService, + illegalSecureStorageService, + this.keyGenerationService, + this.encryptService, + this.logService, ); this.migrationRunner = new MigrationRunner( @@ -207,6 +234,7 @@ export class Main { this.environmentService, this.windowMain, this.updaterMain, + this.desktopSettingsService, ); this.biometricsService = new BiometricsService( @@ -231,11 +259,10 @@ export class Main { app.getPath("exe"), ); + this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider); + this.clipboardMain = new ClipboardMain(); this.clipboardMain.init(); - - this.mainCryptoFunctionService = new MainCryptoFunctionService(); - this.mainCryptoFunctionService.init(); } bootstrap() { @@ -266,10 +293,10 @@ export class Main { if ( (await this.stateService.getEnableBrowserIntegration()) || - (await this.stateService.getEnableDuckDuckGoBrowserIntegration()) + (await firstValueFrom( + this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$, + )) ) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises this.nativeMessagingMain.listen(); } @@ -321,6 +348,19 @@ export class Main { if (!hardwareAcceleration) { this.logService.warning("Hardware acceleration is disabled"); app.disableHardwareAcceleration(); + } else if (isMacAppStore()) { + // We disable hardware acceleration on Mac App Store builds for iMacs with amd switchable GPUs due to: + // https://github.com/electron/electron/issues/41346 + const gpuInfo: any = await app.getGPUInfo("basic"); + const badGpu = gpuInfo?.auxAttributes?.amdSwitchable ?? false; + const isImac = gpuInfo?.machineModelName == "iMac"; + + if (isImac && badGpu) { + this.logService.warning( + "Bad GPU detected, hardware acceleration is disabled for compatibility", + ); + app.disableHardwareAcceleration(); + } } } } diff --git a/apps/desktop/src/main/menu/menu.account.ts b/apps/desktop/src/main/menu/menu.account.ts index 142431ae0d..f3c9b08531 100644 --- a/apps/desktop/src/main/menu/menu.account.ts +++ b/apps/desktop/src/main/menu/menu.account.ts @@ -65,10 +65,10 @@ export class AccountMenu implements IMenubarMenu { id: "changeMasterPass", click: async () => { const result = await dialog.showMessageBox(this._window, { - title: this.localize("changeMasterPass"), - message: this.localize("changeMasterPass"), - detail: this.localize("changeMasterPasswordConfirmation"), - buttons: [this.localize("yes"), this.localize("no")], + title: this.localize("continueToWebApp"), + message: this.localize("continueToWebApp"), + detail: this.localize("changeMasterPasswordOnWebConfirmation"), + buttons: [this.localize("continue"), this.localize("cancel")], cancelId: 1, defaultId: 0, noLink: true, diff --git a/apps/desktop/src/main/menu/menu.help.ts b/apps/desktop/src/main/menu/menu.help.ts index 133c90a6af..7cc0fc2681 100644 --- a/apps/desktop/src/main/menu/menu.help.ts +++ b/apps/desktop/src/main/menu/menu.help.ts @@ -1,7 +1,8 @@ -import { shell, MenuItemConstructorOptions } from "electron"; +import { shell, MenuItemConstructorOptions, app } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { isMacAppStore, isWindowsStore } from "../../utils"; import { AboutMenu } from "./menu.about"; @@ -26,23 +27,23 @@ export class HelpMenu implements IMenubarMenu { this.separator, this.getMobileApp, this.getBrowserExtension, + this.separator, + this.troubleshooting, ]; - if (this._aboutMenu != null) { - items.push(...this._aboutMenu.items); + if (this.aboutMenu != null) { + items.push(...this.aboutMenu.items); } return items; } - private readonly _i18nService: I18nService; - private readonly _webVaultUrl: string; - private readonly _aboutMenu: AboutMenu; - - constructor(i18nService: I18nService, webVaultUrl: string, aboutMenu: AboutMenu) { - this._i18nService = i18nService; - this._webVaultUrl = webVaultUrl; - this._aboutMenu = aboutMenu; - } + constructor( + private i18nService: I18nService, + private desktopSettingsService: DesktopSettingsService, + private webVaultUrl: string, + private hardwareAccelerationEnabled: boolean, + private aboutMenu: AboutMenu, + ) {} private get helpAndFeedback(): MenuItemConstructorOptions { return { @@ -130,7 +131,7 @@ export class HelpMenu implements IMenubarMenu { return { id: "goToWebVault", label: this.localize("goToWebVault"), - click: () => shell.openExternal(this._webVaultUrl), + click: () => shell.openExternal(this.webVaultUrl), }; } @@ -148,25 +149,19 @@ export class HelpMenu implements IMenubarMenu { { id: "iOS", label: "iOS", - click: () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises + click: () => shell.openExternal( "https://itunes.apple.com/app/" + "bitwarden-free-password-manager/id1137397744?mt=8", - ); - }, + ), }, { id: "android", label: "Android", visible: !isMacAppStore(), // Apple Guideline 2.3.10 - Accurate Metadata - click: () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises + click: () => shell.openExternal( "https://play.google.com/store/apps/" + "details?id=com.x8bit.bitwarden", - ); - }, + ), }, ]; } @@ -185,62 +180,78 @@ export class HelpMenu implements IMenubarMenu { { id: "chrome", label: "Chrome", - click: () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises + click: () => shell.openExternal( "https://chromewebstore.google.com/detail/" + "bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb", - ); - }, + ), }, { id: "firefox", label: "Firefox", - click: () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises + click: () => shell.openExternal( "https://addons.mozilla.org/firefox/addon/" + "bitwarden-password-manager/", - ); - }, + ), }, { id: "firefox", label: "Opera", - click: () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises + click: () => shell.openExternal( "https://addons.opera.com/extensions/details/" + "bitwarden-free-password-manager/", - ); - }, + ), }, { id: "firefox", label: "Edge", - click: () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises + click: () => shell.openExternal( "https://microsoftedge.microsoft.com/addons/" + "detail/jbkfoedolllekgbhcbcoahefnbanhhlh", - ); - }, + ), }, { id: "safari", label: "Safari", - click: () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - shell.openExternal("https://bitwarden.com/download/"); + click: () => shell.openExternal("https://bitwarden.com/download/"), + }, + ]; + } + + private get troubleshooting(): MenuItemConstructorOptions { + return { + id: "troubleshooting", + label: this.localize("troubleshooting"), + submenu: this.troubleshootingSubmenu, + }; + } + + private get troubleshootingSubmenu(): MenuItemConstructorOptions[] { + return [ + { + id: "hardwareAcceleration", + label: this.localize( + this.hardwareAccelerationEnabled + ? "disableHardwareAccelerationRestart" + : "enableHardwareAccelerationRestart", + ), + click: async () => { + await this.desktopSettingsService.setHardwareAcceleration( + !this.hardwareAccelerationEnabled, + ); + // `app.relaunch` crashes the app on Mac Store builds. Disabling it for now. + // https://github.com/electron/electron/issues/41690 + if (!isMacAppStore()) { + app.relaunch(); + } + app.exit(); }, }, ]; } private localize(s: string) { - return this._i18nService.t(s); + return this.i18nService.t(s); } } diff --git a/apps/desktop/src/main/menu/menu.main.ts b/apps/desktop/src/main/menu/menu.main.ts index 4d3dc0d62e..9a63d389b5 100644 --- a/apps/desktop/src/main/menu/menu.main.ts +++ b/apps/desktop/src/main/menu/menu.main.ts @@ -5,6 +5,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { UpdaterMain } from "../updater.main"; import { WindowMain } from "../window.main"; @@ -20,6 +21,7 @@ export class MenuMain { private environmentService: EnvironmentService, private windowMain: WindowMain, private updaterMain: UpdaterMain, + private desktopSettingsService: DesktopSettingsService, ) {} async init() { @@ -36,10 +38,12 @@ export class MenuMain { new Menubar( this.i18nService, this.messagingService, + this.desktopSettingsService, this.updaterMain, this.windowMain, await this.getWebVaultUrl(), app.getVersion(), + await firstValueFrom(this.desktopSettingsService.hardwareAcceleration$), updateRequest, ).menu, ); diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index da55ed951c..eb1dacf825 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -3,6 +3,7 @@ import { Menu, MenuItemConstructorOptions } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { isMac } from "../../utils"; import { UpdaterMain } from "../updater.main"; import { WindowMain } from "../window.main"; @@ -47,10 +48,12 @@ export class Menubar { constructor( i18nService: I18nService, messagingService: MessagingService, + desktopSettingsService: DesktopSettingsService, updaterMain: UpdaterMain, windowMain: WindowMain, webVaultUrl: string, appVersion: string, + hardwareAccelerationEnabled: boolean, updateRequest?: MenuUpdateRequest, ) { let isLocked = true; @@ -89,7 +92,9 @@ export class Menubar { new WindowMenu(i18nService, messagingService, windowMain), new HelpMenu( i18nService, + desktopSettingsService, webVaultUrl, + hardwareAccelerationEnabled, new AboutMenu(i18nService, appVersion, windowMain.win, updaterMain), ), ]; diff --git a/apps/desktop/src/main/messaging.main.ts b/apps/desktop/src/main/messaging.main.ts index cc67e312b5..256d551560 100644 --- a/apps/desktop/src/main/messaging.main.ts +++ b/apps/desktop/src/main/messaging.main.ts @@ -77,14 +77,10 @@ export class MessagingMain { break; case "enableBrowserIntegration": this.main.nativeMessagingMain.generateManifests(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises this.main.nativeMessagingMain.listen(); break; case "enableDuckDuckGoBrowserIntegration": this.main.nativeMessagingMain.generateDdgManifests(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises this.main.nativeMessagingMain.listen(); break; case "disableBrowserIntegration": diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 77a0b31314..05e987e20b 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -24,7 +24,7 @@ export class NativeMessagingMain { private exePath: string, ) {} - async listen() { + listen() { ipc.config.id = "bitwarden"; ipc.config.retry = 1500; const ipcSocketRoot = getIpcSocketRoot(); diff --git a/apps/desktop/src/main/updater.main.ts b/apps/desktop/src/main/updater.main.ts index c9a2dc0e14..0e2efa66f9 100644 --- a/apps/desktop/src/main/updater.main.ts +++ b/apps/desktop/src/main/updater.main.ts @@ -27,8 +27,7 @@ export class UpdaterMain { process.platform === "win32" && !isWindowsStore() && !isWindowsPortable(); const macCanUpdate = process.platform === "darwin" && !isMacAppStore(); this.canUpdate = - process.env.ELECTRON_NO_UPDATER !== "1" && - (linuxCanUpdate || windowsCanUpdate || macCanUpdate); + !this.userDisabledUpdates() && (linuxCanUpdate || windowsCanUpdate || macCanUpdate); } async init() { @@ -144,4 +143,13 @@ export class UpdaterMain { autoUpdater.autoDownload = true; this.doingUpdateCheck = false; } + + private userDisabledUpdates(): boolean { + for (const arg of process.argv) { + if (arg != null && arg.toUpperCase().indexOf("--ELECTRON_NO_UPDATER=1") > -1) { + return true; + } + } + return process.env.ELECTRON_NO_UPDATER === "1"; + } } diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 2862977ca2..11b38bd273 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,27 +1,615 @@ { "name": "@bitwarden/desktop", - "version": "2024.3.1", + "version": "2024.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.3.1", + "version": "2024.4.2", "license": "GPL-3.0", "dependencies": { - "@bitwarden/desktop-native": "file:../desktop_native" + "@bitwarden/desktop-native": "file:../desktop_native", + "argon2": "0.31.0" } }, "../desktop_native": { "version": "0.1.0", "license": "GPL-3.0", "devDependencies": { - "@napi-rs/cli": "2.14.8" + "@napi-rs/cli": "2.16.2" } }, "node_modules/@bitwarden/desktop-native": { "resolved": "../desktop_native", "link": true + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argon2": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.31.0.tgz", + "integrity": "sha512-r56NWwlE3tjD/FIqL1T+V4Ka+Mb5yMF35w1YWHpwpEjeONXBUbxmjhWkWqY63mse8lpcZ+ZZIGpKL+s+qXhyfg==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "@phc/format": "^1.0.0", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 4c748c4a05..a65dab016c 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.3.1", + "version": "2024.4.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/flags.ts b/apps/desktop/src/platform/flags.ts index 0481c8ce9b..dc0103e243 100644 --- a/apps/desktop/src/platform/flags.ts +++ b/apps/desktop/src/platform/flags.ts @@ -7,13 +7,11 @@ import { } from "@bitwarden/common/platform/misc/flags"; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ -export type Flags = { - showDDGSetting?: boolean; -} & SharedFlags; +// eslint-disable-next-line @typescript-eslint/ban-types +export type Flags = {} & SharedFlags; // required to avoid linting errors when there are no flags -/* eslint-disable-next-line @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = {} & SharedDevFlags; export function flagEnabled(flag: keyof Flags): boolean { diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts index 2d5c1d19eb..fb7ce048b5 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts @@ -1,6 +1,6 @@ export abstract class BiometricsServiceAbstraction { - osSupportsBiometric: () => Promise; - canAuthBiometric: ({ + abstract osSupportsBiometric(): Promise; + abstract canAuthBiometric({ service, key, userId, @@ -8,11 +8,11 @@ export abstract class BiometricsServiceAbstraction { service: string; key: string; userId: string; - }) => Promise; - authenticateBiometric: () => Promise; - getBiometricKey: (service: string, key: string) => Promise; - setBiometricKey: (service: string, key: string, value: string) => Promise; - setEncryptionKeyHalf: ({ + }): Promise; + abstract authenticateBiometric(): Promise; + abstract getBiometricKey(service: string, key: string): Promise; + abstract setBiometricKey(service: string, key: string, value: string): Promise; + abstract setEncryptionKeyHalf({ service, key, value, @@ -20,23 +20,23 @@ export abstract class BiometricsServiceAbstraction { service: string; key: string; value: string; - }) => void; - deleteBiometricKey: (service: string, key: string) => Promise; + }): void; + abstract deleteBiometricKey(service: string, key: string): Promise; } export interface OsBiometricService { - osSupportsBiometric: () => Promise; - authenticateBiometric: () => Promise; - getBiometricKey: ( + osSupportsBiometric(): Promise; + authenticateBiometric(): Promise; + getBiometricKey( service: string, key: string, clientKeyHalfB64: string | undefined, - ) => Promise; - setBiometricKey: ( + ): Promise; + setBiometricKey( service: string, key: string, value: string, clientKeyHalfB64: string | undefined, - ) => Promise; - deleteBiometricKey: (service: string, key: string) => Promise; + ): Promise; + deleteBiometricKey(service: string, key: string): Promise; } diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 04adfcac70..3d9171b52e 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -1,6 +1,7 @@ import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { mock } from "jest-mock-extended"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -30,6 +31,7 @@ describe("electronCryptoService", () => { const platformUtilService = mock(); const logService = mock(); const stateService = mock(); + let masterPasswordService: FakeMasterPasswordService; let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock(); @@ -38,9 +40,11 @@ describe("electronCryptoService", () => { beforeEach(() => { accountService = mockAccountServiceWith("userId" as UserId); + masterPasswordService = new FakeMasterPasswordService(); stateProvider = new FakeStateProvider(accountService); sut = new ElectronCryptoService( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 6b9327a9c4..d113a18200 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -20,6 +21,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export class ElectronCryptoService extends CryptoService { constructor( + masterPasswordService: InternalMasterPasswordServiceAbstraction, keyGenerationService: KeyGenerationService, cryptoFunctionService: CryptoFunctionService, encryptService: EncryptService, @@ -31,6 +33,7 @@ export class ElectronCryptoService extends CryptoService { private biometricStateService: BiometricStateService, ) { super( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, @@ -159,12 +162,16 @@ export class ElectronCryptoService extends CryptoService { const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId }); // decrypt const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; - let encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); - encUserKey = encUserKey ?? (await this.stateService.getMasterKeyEncryptedUserKey()); + userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); + const encUserKey = + encUserKeyPrim != null + ? new EncString(encUserKeyPrim) + : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); if (!encUserKey) { throw new Error("No user key found during biometric migration"); } - const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); + const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey); // migrate await this.storeBiometricKey(userKey, userId); await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); diff --git a/apps/desktop/src/platform/services/electron-state.service.ts b/apps/desktop/src/platform/services/electron-state.service.ts index f4399221d2..33c97f48af 100644 --- a/apps/desktop/src/platform/services/electron-state.service.ts +++ b/apps/desktop/src/platform/services/electron-state.service.ts @@ -1,47 +1,12 @@ -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; -import { DeviceKey } from "@bitwarden/common/types/key"; import { Account } from "../../models/account"; export class ElectronStateService extends BaseStateService { - private partialKeys = { - deviceKey: "_deviceKey", - }; - async addAccount(account: Account) { // Apply desktop overides to default account values account = new Account(account); await super.addAccount(account); } - - override async getDeviceKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - - const b64DeviceKey = await this.secureStorageService.get( - `${options.userId}${this.partialKeys.deviceKey}`, - options, - ); - - if (b64DeviceKey == null) { - return null; - } - - return new SymmetricCryptoKey(Utils.fromB64ToArray(b64DeviceKey)) as DeviceKey; - } - - override async setDeviceKey(value: DeviceKey, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); - if (options?.userId == null) { - return; - } - - await this.saveSecureStorageKey(this.partialKeys.deviceKey, value.keyB64, options); - } } diff --git a/apps/desktop/src/platform/services/i18n.main.service.ts b/apps/desktop/src/platform/services/i18n.main.service.ts index edf79eccf0..bb2d1b1c1c 100644 --- a/apps/desktop/src/platform/services/i18n.main.service.ts +++ b/apps/desktop/src/platform/services/i18n.main.service.ts @@ -35,6 +35,7 @@ export class I18nMainService extends BaseI18nService { "bs", "ca", "cs", + "cy", "da", "de", "el", @@ -48,6 +49,7 @@ export class I18nMainService extends BaseI18nService { "fi", "fil", "fr", + "gl", "he", "hi", "hr", @@ -59,13 +61,18 @@ export class I18nMainService extends BaseI18nService { "km", "kn", "ko", + "lt", "lv", "me", "ml", + "mr", + "my", "nb", + "ne", "nl", "nn", "pl", + "or", "pt-BR", "pt-PT", "ro", @@ -75,6 +82,7 @@ export class I18nMainService extends BaseI18nService { "sl", "sr", "sv", + "te", "th", "tr", "uk", diff --git a/apps/desktop/src/platform/services/i18n.renderer.service.ts b/apps/desktop/src/platform/services/i18n.renderer.service.ts index 87ad8b4018..18fe588f77 100644 --- a/apps/desktop/src/platform/services/i18n.renderer.service.ts +++ b/apps/desktop/src/platform/services/i18n.renderer.service.ts @@ -28,6 +28,7 @@ export class I18nRendererService extends BaseI18nService { "bs", "ca", "cs", + "cy", "da", "de", "el", @@ -41,6 +42,7 @@ export class I18nRendererService extends BaseI18nService { "fi", "fil", "fr", + "gl", "he", "hi", "hr", @@ -52,13 +54,18 @@ export class I18nRendererService extends BaseI18nService { "km", "kn", "ko", + "lt", "lv", "me", "ml", + "mr", + "my", "nb", + "ne", "nl", "nn", "pl", + "or", "pt-BR", "pt-PT", "ro", @@ -68,6 +75,7 @@ export class I18nRendererService extends BaseI18nService { "sl", "sr", "sv", + "te", "th", "tr", "uk", diff --git a/apps/desktop/src/platform/services/illegal-secure-storage.service.ts b/apps/desktop/src/platform/services/illegal-secure-storage.service.ts new file mode 100644 index 0000000000..12f86226be --- /dev/null +++ b/apps/desktop/src/platform/services/illegal-secure-storage.service.ts @@ -0,0 +1,28 @@ +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; +import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; + +export class IllegalSecureStorageService implements AbstractStorageService { + constructor() {} + + get valuesRequireDeserialization(): boolean { + throw new Error("Method not implemented."); + } + has(key: string, options?: StorageOptions): Promise { + throw new Error("Method not implemented."); + } + save(key: string, obj: T, options?: StorageOptions): Promise { + throw new Error("Method not implemented."); + } + async get(key: string): Promise { + throw new Error("Method not implemented."); + } + async set(key: string, obj: T): Promise { + throw new Error("Method not implemented."); + } + async remove(key: string): Promise { + throw new Error("Method not implemented."); + } + async clear(): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/apps/desktop/src/services/native-message-handler.service.ts b/apps/desktop/src/services/native-message-handler.service.ts index c90271d25c..ebe1ee6248 100644 --- a/apps/desktop/src/services/native-message-handler.service.ts +++ b/apps/desktop/src/services/native-message-handler.service.ts @@ -5,13 +5,14 @@ import { NativeMessagingVersion } from "@bitwarden/common/enums"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { StateService } from "@bitwarden/common/platform/services/state.service"; import { DialogService } from "@bitwarden/components"; import { VerifyNativeMessagingDialogComponent } from "../app/components/verify-native-messaging-dialog.component"; +import { DesktopAutofillSettingsService } from "../autofill/services/desktop-autofill-settings.service"; import { DecryptedCommandData } from "../models/native-messaging/decrypted-command-data"; import { EncryptedMessage } from "../models/native-messaging/encrypted-message"; import { EncryptedMessageResponse } from "../models/native-messaging/encrypted-message-response"; @@ -34,6 +35,7 @@ export class NativeMessageHandlerService { private messagingService: MessagingService, private encryptedMessageHandlerService: EncryptedMessageHandlerService, private dialogService: DialogService, + private desktopAutofillSettingsService: DesktopAutofillSettingsService, ) {} async handleMessage(message: Message) { @@ -71,7 +73,9 @@ export class NativeMessageHandlerService { try { const remotePublicKey = Utils.fromB64ToArray(publicKey); - const ddgEnabled = await this.stateService.getEnableDuckDuckGoBrowserIntegration(); + const ddgEnabled = await firstValueFrom( + this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$, + ); if (!ddgEnabled) { this.sendResponse({ diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 148e4f1e89..01d9476977 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -1,6 +1,7 @@ import { Injectable, NgZone } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -30,6 +31,7 @@ export class NativeMessagingService { private sharedSecrets = new Map(); constructor( + private masterPasswordService: MasterPasswordServiceAbstraction, private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private platformUtilService: PlatformUtilsService, @@ -162,7 +164,9 @@ export class NativeMessagingService { KeySuffixOptions.Biometric, message.userId, ); - const masterKey = await this.cryptoService.getMasterKey(message.userId); + const masterKey = await firstValueFrom( + this.masterPasswordService.masterKey$(message.userId as UserId), + ); if (userKey != null) { // we send the master key still for backwards compatibility diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.html b/apps/desktop/src/vault/app/vault/add-edit.component.html index 43add53254..ea7be92935 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit.component.html @@ -116,14 +116,25 @@
- {{ "typePasskey" | i18n }} - {{ fido2CredentialCreationDateValue }} + +
+ {{ "typePasskey" | i18n }} + {{ fido2CredentialCreationDateValue }} +
diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index 8532b7462a..86e0b881ee 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -8,7 +8,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -49,7 +49,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges, sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, ) { super( cipherService, @@ -75,6 +75,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges, async ngOnInit() { await super.ngOnInit(); + await this.load(); this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.ngZone.run(() => { switch (message.command) { diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index e8aabbb20f..208bbc70f0 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -8,7 +8,7 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { firstValueFrom, Subject, takeUntil } from "rxjs"; import { first } from "rxjs/operators"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -16,13 +16,13 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -32,6 +32,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { AuthRequestServiceAbstraction } from "../../../../../../libs/auth/src/common/abstractions"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; import { GeneratorComponent } from "../../../app/tools/generator.component"; import { invokeMenu, RendererMenuItem } from "../../../utils"; @@ -102,11 +103,12 @@ export class VaultComponent implements OnInit, OnDestroy { private eventCollectionService: EventCollectionService, private totpService: TotpService, private passwordRepromptService: PasswordRepromptService, - private stateService: StateService, private searchBarService: SearchBarService, private apiService: ApiService, private dialogService: DialogService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private authRequestService: AuthRequestServiceAbstraction, + private accountService: AccountService, ) {} async ngOnInit() { @@ -224,7 +226,8 @@ export class VaultComponent implements OnInit, OnDestroy { this.searchBarService.setEnabled(true); this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault")); - const approveLoginRequests = await this.stateService.getApproveLoginRequests(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const approveLoginRequests = await this.authRequestService.getAcceptAuthRequests(userId); if (approveLoginRequests) { const authRequest = await this.apiService.getLastAuthRequest(); if (authRequest != null) { diff --git a/apps/web/config/base.json b/apps/web/config/base.json index a377298c63..5dc03a4633 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -11,7 +11,6 @@ "allowedHosts": "auto" }, "flags": { - "secretsManager": false, "showPasswordless": false, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index 6e5c65af1d..3faa292692 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -17,7 +17,6 @@ "proxyNotifications": "https://notifications.bitwarden.com" }, "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 97742aee3d..44391a7450 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -20,7 +20,6 @@ } ], "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false }, diff --git a/apps/web/config/euprd.json b/apps/web/config/euprd.json index 4b6c9fa909..72f0c1857d 100644 --- a/apps/web/config/euprd.json +++ b/apps/web/config/euprd.json @@ -11,7 +11,6 @@ "buttonAction": "https://www.paypal.com/cgi-bin/webscr" }, "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/euqa.json b/apps/web/config/euqa.json index 70caf3db62..5f74eb8829 100644 --- a/apps/web/config/euqa.json +++ b/apps/web/config/euqa.json @@ -21,7 +21,6 @@ } ], "flags": { - "secretsManager": true, "showPasswordless": true } } diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index 0ce5f3dc7f..ac36b10784 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -27,7 +27,6 @@ } ], "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index e16df20ad5..7e916a1116 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -7,7 +7,6 @@ "port": 8081 }, "flags": { - "secretsManager": true, "showPasswordless": true, "enableCipherKeyEncryption": false } diff --git a/apps/web/config/usdev.json b/apps/web/config/usdev.json index 9b794d896d..af96a38c6a 100644 --- a/apps/web/config/usdev.json +++ b/apps/web/config/usdev.json @@ -5,7 +5,6 @@ "scim": "https://scim.usdev.bitwarden.pw" }, "flags": { - "secretsManager": true, "showPasswordless": true } } diff --git a/apps/web/jest.config.js b/apps/web/jest.config.js index cde02cd995..f121823ade 100644 --- a/apps/web/jest.config.js +++ b/apps/web/jest.config.js @@ -9,7 +9,11 @@ module.exports = { ...sharedConfig, preset: "jest-preset-angular", setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in web tests + { "@bitwarden/common/spec": ["../../libs/common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "/", + }, + ), }; diff --git a/apps/web/package.json b/apps/web/package.json index 3fad4b14ae..55fe0987d7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.3.0", + "version": "2024.4.1", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", @@ -8,7 +8,7 @@ "build:bit:watch": "webpack serve -c ../../bitwarden_license/bit-web/webpack.config.js", "build:bit:dev": "cross-env ENV=development npm run build:bit", "build:bit:dev:analyze": "cross-env LOGGING=false webpack -c ../../bitwarden_license/bit-web/webpack.config.js --profile --json > stats.json && npx webpack-bundle-analyzer stats.json build/", - "build:bit:dev:watch": "cross-env ENV=development npm run build:bit:watch", + "build:bit:dev:watch": "cross-env ENV=development NODE_OPTIONS=\"--max-old-space-size=8192\" npm run build:bit:watch", "build:bit:qa": "cross-env NODE_ENV=production ENV=qa npm run build:bit", "build:bit:euprd": "cross-env NODE_ENV=production ENV=euprd npm run build:bit", "build:bit:euqa": "cross-env NODE_ENV=production ENV=euqa npm run build:bit", diff --git a/apps/web/src/app/admin-console/common/base.people.component.ts b/apps/web/src/app/admin-console/common/base.people.component.ts index 0a1f4338ff..fbb9faf569 100644 --- a/apps/web/src/app/admin-console/common/base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base.people.component.ts @@ -1,5 +1,5 @@ -import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; -import { firstValueFrom } from "rxjs"; +import { Directive, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -32,8 +32,10 @@ const MaxCheckedCount = 500; @Directive() export abstract class BasePeopleComponent< - UserType extends ProviderUserUserDetailsResponse | OrganizationUserView, -> { + UserType extends ProviderUserUserDetailsResponse | OrganizationUserView, + > + implements OnInit, OnDestroy +{ @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef; @@ -88,7 +90,6 @@ export abstract class BasePeopleComponent< status: StatusType; users: UserType[] = []; pagedUsers: UserType[] = []; - searchText: string; actionPromise: Promise; protected allUsers: UserType[] = []; @@ -97,7 +98,19 @@ export abstract class BasePeopleComponent< protected didScroll = false; protected pageSize = 100; + protected destroy$ = new Subject(); + private pagedUsersCount = 0; + private _searchText$ = new BehaviorSubject(""); + private isSearching: boolean = false; + + get searchText() { + return this._searchText$.value; + } + + set searchText(value: string) { + this._searchText$.next(value); + } constructor( protected apiService: ApiService, @@ -122,6 +135,22 @@ export abstract class BasePeopleComponent< abstract reinviteUser(id: string): Promise; abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise; + ngOnInit(): void { + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearching = isSearchable; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + async load() { const response = await this.getUsers(); this.statusMap.clear(); @@ -390,12 +419,8 @@ export abstract class BasePeopleComponent< } } - isSearching() { - return this.searchService.isSearchable(this.searchText); - } - isPaging() { - const searching = this.isSearching(); + const searching = this.isSearching; if (searching && this.didScroll) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/web/src/app/admin-console/icons/devices.ts b/apps/web/src/app/admin-console/icons/devices.ts index 348c836c4b..9faddb0b2e 100644 --- a/apps/web/src/app/admin-console/icons/devices.ts +++ b/apps/web/src/app/admin-console/icons/devices.ts @@ -3,15 +3,15 @@ import { svgIcon } from "@bitwarden/components"; export const Devices = svgIcon` - - - - - - - - - + + + + + + + + + `; diff --git a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts b/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts index 33a3069e1d..63431cd6ab 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts @@ -3,7 +3,7 @@ import { Injectable } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CoreOrganizationModule } from "../../core-organization.module"; import { GroupView } from "../../views/group.view"; @@ -18,7 +18,7 @@ import { GroupDetailsResponse, GroupResponse } from "./responses/group.response" export class GroupService { constructor( protected apiService: ApiService, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) {} async get(orgId: string, groupId: string): Promise { @@ -52,7 +52,7 @@ export class GroupService { export class InternalGroupService extends GroupService { constructor( protected apiService: ApiService, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) { super(apiService, configService); } diff --git a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts index a1d1bc3e23..399140e3ea 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts @@ -6,7 +6,7 @@ import { OrganizationUserUpdateRequest, } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { OrganizationUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CoreOrganizationModule } from "../core-organization.module"; import { OrganizationUserAdminView } from "../views/organization-user-admin-view"; @@ -14,7 +14,7 @@ import { OrganizationUserAdminView } from "../views/organization-user-admin-view @Injectable({ providedIn: CoreOrganizationModule }) export class UserAdminService { constructor( - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private organizationUserService: OrganizationUserService, ) {} diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts index ee263ec750..947ae9b13e 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts @@ -26,6 +26,10 @@ export class OrganizationUserView { twoFactorEnabled: boolean; usesKeyConnector: boolean; hasMasterPassword: boolean; + /** + * True if this organizaztion user has been granted access to Secrets Manager, false otherwise. + */ + accessSecretsManager: boolean; collections: CollectionAccessSelectionView[] = []; groups: string[] = []; diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.html b/apps/web/src/app/admin-console/organizations/create/organization-information.component.html index 6029cfd833..e0a8006081 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.html +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.html @@ -20,19 +20,4 @@
-
- - {{ "accountOwnedBusiness" | i18n }} -
- - {{ "businessName" | i18n }} - - -
-
diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts index 99cb3102aa..602ad82972 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts @@ -1,15 +1,32 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { UntypedFormGroup } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @Component({ selector: "app-org-info", templateUrl: "organization-information.component.html", }) -export class OrganizationInformationComponent { +export class OrganizationInformationComponent implements OnInit { @Input() nameOnly = false; @Input() createOrganization = true; @Input() isProvider = false; @Input() acceptingSponsorship = false; @Input() formGroup: UntypedFormGroup; @Output() changedBusinessOwned = new EventEmitter(); + + constructor(private accountService: AccountService) {} + + async ngOnInit(): Promise { + if (this.formGroup.controls.billingEmail.value) { + return; + } + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + if (activeAccount?.email) { + this.formGroup.controls.billingEmail.setValue(activeAccount.email); + } + } } diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 348c410075..2b3be14974 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -3,7 +3,7 @@ - + + + diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 90010160aa..b1a84c22f3 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -15,14 +15,17 @@ import { getOrganizationById, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; import { PaymentMethodWarningsModule } from "../../../billing/shared"; import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component"; +import { ToggleWidthComponent } from "../../../layouts/toggle-width.component"; import { AdminConsoleLogo } from "../../icons/admin-console-logo"; @Component({ @@ -39,6 +42,7 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo"; OrgSwitcherComponent, BannerModule, PaymentMethodWarningsModule, + ToggleWidthComponent, ], }) export class OrganizationLayoutComponent implements OnInit, OnDestroy { @@ -48,6 +52,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { organization$: Observable; showPaymentAndHistory$: Observable; + hideNewOrgButton$: Observable; private _destroy = new Subject(); @@ -61,6 +66,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private platformUtilsService: PlatformUtilsService, private configService: ConfigService, + private policyService: PolicyService, ) {} async ngOnInit() { @@ -85,6 +91,8 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { org?.canEditPaymentMethods, ), ); + + this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg); } ngOnDestroy() { diff --git a/apps/web/src/app/admin-console/organizations/manage/events.component.ts b/apps/web/src/app/admin-console/organizations/manage/events.component.ts index 20a0ef6e42..9fb9015155 100644 --- a/apps/web/src/app/admin-console/organizations/manage/events.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/events.component.ts @@ -154,7 +154,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe if (r.serviceAccountId) { return { - name: this.i18nService.t("serviceAccount") + " " + this.getShortId(r.serviceAccountId), + name: this.i18nService.t("machineAccount") + " " + this.getShortId(r.serviceAccountId), }; } diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index c6bfb94557..3afb816e14 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -31,7 +31,12 @@ -

{{ "editGroupMembersDesc" | i18n }}

+

+ {{ "editGroupMembersDesc" | i18n }} + + {{ "restrictedGroupAccessDesc" | i18n }} + +

= []; group: GroupView; groupForm = this.formBuilder.group({ @@ -110,6 +125,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { return this.params.organizationId; } + protected get editMode(): boolean { + return this.groupId != null; + } + private destroy$ = new Subject(); private get orgCollections$() { @@ -134,7 +153,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { ); } - private get orgMembers$() { + private get orgMembers$(): Observable> { return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe( map((response) => response.data.map((m) => ({ @@ -145,34 +164,55 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email, labelName: m.name || m.email, status: m.status, + userId: m.userId as UserId, })), ), ); } - private get groupDetails$() { - if (!this.editMode) { - return of(undefined); - } - - return combineLatest([ - this.groupService.get(this.organizationId, this.groupId), - this.apiService.getGroupUsers(this.organizationId, this.groupId), - ]).pipe( - map(([groupView, users]) => { - groupView.members = users; - return groupView; - }), - catchError((e: unknown) => { - if (e instanceof ErrorResponse) { - this.logService.error(e.message); - } else { - this.logService.error(e.toString()); - } + private groupDetails$: Observable = of(this.editMode).pipe( + concatMap((editMode) => { + if (!editMode) { return of(undefined); - }), - ); - } + } + + return combineLatest([ + this.groupService.get(this.organizationId, this.groupId), + this.apiService.getGroupUsers(this.organizationId, this.groupId), + ]).pipe( + map(([groupView, users]) => { + groupView.members = users; + return groupView; + }), + catchError((e: unknown) => { + if (e instanceof ErrorResponse) { + this.logService.error(e.message); + } else { + this.logService.error(e.toString()); + } + return of(undefined); + }), + ); + }), + shareReplay({ refCount: false }), + ); + + restrictGroupAccess$ = combineLatest([ + this.organizationService.get$(this.organizationId), + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), + this.groupDetails$, + ]).pipe( + map( + ([organization, flexibleCollectionsV1Enabled, group]) => + // Feature flag conditionals + flexibleCollectionsV1Enabled && + organization.flexibleCollections && + // Business logic conditionals + !organization.allowAdminAccessToAllCollectionItems && + group !== undefined, + ), + shareReplay({ refCount: true, bufferSize: 1 }), + ); constructor( @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, @@ -188,17 +228,25 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private dialogService: DialogService, private organizationService: OrganizationService, + private configService: ConfigService, + private accountService: AccountService, ) { this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; } ngOnInit() { - this.editMode = this.loading = this.groupId != null; + this.loading = true; this.title = this.i18nService.t(this.editMode ? "editGroup" : "newGroup"); - combineLatest([this.orgCollections$, this.orgMembers$, this.groupDetails$]) + combineLatest([ + this.orgCollections$, + this.orgMembers$, + this.groupDetails$, + this.restrictGroupAccess$, + this.accountService.activeAccount$, + ]) .pipe(takeUntil(this.destroy$)) - .subscribe(([collections, members, group]) => { + .subscribe(([collections, members, group, restrictGroupAccess, activeAccount]) => { this.collections = collections; this.members = members; this.group = group; @@ -224,6 +272,18 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { }); } + // If the current user is not already in the group and cannot add themselves, remove them from the list + if (restrictGroupAccess) { + const organizationUserId = this.members.find((m) => m.userId === activeAccount.id).id; + const isAlreadyInGroup = this.groupForm.value.members.some( + (m) => m.id === organizationUserId, + ); + + if (!isAlreadyInGroup) { + this.members = this.members.filter((m) => m.id !== organizationUserId); + } + } + this.loading = false; }); } diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index a41d57f874..9ff596181e 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -91,15 +91,16 @@ export class GroupsComponent implements OnInit, OnDestroy { private pagedGroupsCount = 0; private pagedGroups: GroupDetailsRow[]; private searchedGroups: GroupDetailsRow[]; - private _searchText: string; + private _searchText$ = new BehaviorSubject(""); private destroy$ = new Subject(); private refreshGroups$ = new BehaviorSubject(null); + private isSearching: boolean = false; get searchText() { - return this._searchText; + return this._searchText$.value; } set searchText(value: string) { - this._searchText = value; + this._searchText$.next(value); // Manually update as we are not using the search pipe in the template this.updateSearchedGroups(); } @@ -114,7 +115,7 @@ export class GroupsComponent implements OnInit, OnDestroy { if (this.isPaging()) { return this.pagedGroups; } - if (this.isSearching()) { + if (this.isSearching) { return this.searchedGroups; } return this.groups; @@ -180,6 +181,15 @@ export class GroupsComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this._searchText$ + .pipe( + switchMap((searchText) => this.searchService.isSearchable(searchText)), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearching = isSearchable; + }); } ngOnDestroy() { @@ -297,10 +307,6 @@ export class GroupsComponent implements OnInit, OnDestroy { this.loadMore(); } - isSearching() { - return this.searchService.isSearchable(this.searchText); - } - check(groupRow: GroupDetailsRow) { groupRow.checked = !groupRow.checked; } @@ -310,7 +316,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } isPaging() { - const searching = this.isSearching(); + const searching = this.isSearching; if (searching && this.didScroll) { this.resetPaging(); } @@ -340,7 +346,7 @@ export class GroupsComponent implements OnInit, OnDestroy { } private updateSearchedGroups() { - if (this.searchService.isSearchable(this.searchText)) { + if (this.isSearching) { // Making use of the pipe in the component as we need know which groups where filtered this.searchedGroups = this.searchPipe.transform( this.groups, diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 668b09eb7e..771b8cc505 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -24,13 +24,14 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { DialogService } from "@bitwarden/components"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; +import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view"; import { CollectionAccessSelectionView, GroupService, @@ -148,7 +149,7 @@ export class MemberDialogComponent implements OnDestroy { private userService: UserAdminService, private organizationUserService: OrganizationUserService, private dialogService: DialogService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private accountService: AccountService, organizationService: OrganizationService, ) { @@ -206,25 +207,52 @@ export class MemberDialogComponent implements OnDestroy { collections: this.collectionAdminService.getAll(this.params.organizationId), userDetails: userDetails$, groups: groups$, + flexibleCollectionsV1Enabled: this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollectionsV1, + false, + ), }) .pipe(takeUntil(this.destroy$)) - .subscribe(({ organization, collections, userDetails, groups }) => { - this.setFormValidators(organization); + .subscribe( + ({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => { + this.setFormValidators(organization); - this.collectionAccessItems = [].concat( - collections.map((c) => mapCollectionToAccessItemView(c)), - ); + // Groups tab: populate available groups + this.groupAccessItems = [].concat( + groups.map((g) => mapGroupToAccessItemView(g)), + ); - this.groupAccessItems = [].concat( - groups.map((g) => mapGroupToAccessItemView(g)), - ); + // Collections tab: Populate all available collections (including current user access where applicable) + this.collectionAccessItems = collections + .map((c) => + mapCollectionToAccessItemView( + c, + organization, + flexibleCollectionsV1Enabled, + userDetails == null + ? undefined + : c.users.find((access) => access.id === userDetails.id), + ), + ) + // But remove collections that we can't assign access to, unless the user is already assigned + .filter( + (item) => + !item.readonly || userDetails?.collections.some((access) => access.id == item.id), + ); - if (this.params.organizationUserId) { - this.loadOrganizationUser(userDetails, groups, collections); - } + if (userDetails != null) { + this.loadOrganizationUser( + userDetails, + groups, + collections, + organization, + flexibleCollectionsV1Enabled, + ); + } - this.loading = false; - }); + this.loading = false; + }, + ); } private setFormValidators(organization: Organization) { @@ -246,7 +274,9 @@ export class MemberDialogComponent implements OnDestroy { private loadOrganizationUser( userDetails: OrganizationUserAdminView, groups: GroupView[], - collections: CollectionView[], + collections: CollectionAdminView[], + organization: Organization, + flexibleCollectionsV1Enabled: boolean, ) { if (!userDetails) { throw new Error("Could not find user to edit."); @@ -295,13 +325,22 @@ export class MemberDialogComponent implements OnDestroy { }), ); + // Populate additional collection access via groups (rendered as separate rows from user access) this.collectionAccessItems = this.collectionAccessItems.concat( collectionsFromGroups.map(({ collection, accessSelection, group }) => - mapCollectionToAccessItemView(collection, accessSelection, group), + mapCollectionToAccessItemView( + collection, + organization, + flexibleCollectionsV1Enabled, + accessSelection, + group, + ), ), ); - const accessSelections = mapToAccessSelections(userDetails); + // Set current collections and groups the user has access to (excluding collections the current user doesn't have + // permissions to change - they are included as readonly via the CollectionAccessItems) + const accessSelections = mapToAccessSelections(userDetails, this.collectionAccessItems); const groupAccessSelections = mapToGroupAccessSelections(userDetails.groups); this.formGroup.removeControl("emails"); @@ -573,6 +612,8 @@ export class MemberDialogComponent implements OnDestroy { function mapCollectionToAccessItemView( collection: CollectionView, + organization: Organization, + flexibleCollectionsV1Enabled: boolean, accessSelection?: CollectionAccessSelectionView, group?: GroupView, ): AccessItemView { @@ -581,7 +622,8 @@ function mapCollectionToAccessItemView( id: group ? `${collection.id}-${group.id}` : collection.id, labelName: collection.name, listName: collection.name, - readonly: group !== undefined, + readonly: + group !== undefined || !collection.canEdit(organization, flexibleCollectionsV1Enabled), readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined, viaGroupName: group?.name, }; @@ -596,16 +638,23 @@ function mapGroupToAccessItemView(group: GroupView): AccessItemView { }; } -function mapToAccessSelections(user: OrganizationUserAdminView): AccessItemValue[] { +function mapToAccessSelections( + user: OrganizationUserAdminView, + items: AccessItemView[], +): AccessItemValue[] { if (user == undefined) { return []; } - return [].concat( - user.collections.map((selection) => ({ - id: selection.id, - type: AccessItemType.Collection, - permission: convertToPermission(selection), - })), + + return ( + user.collections + // The FormControl value only represents editable collection access - exclude readonly access selections + .filter((selection) => !items.find((item) => item.id == selection.id).readonly) + .map((selection) => ({ + id: selection.id, + type: AccessItemType.Collection, + permission: convertToPermission(selection), + })) ); } diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index b2aedacc80..6b632dce38 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, @@ -9,7 +9,6 @@ import { map, Observable, shareReplay, - Subject, switchMap, takeUntil, } from "rxjs"; @@ -52,7 +51,6 @@ import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; -import { flagEnabled } from "../../../../utils/flags"; import { openEntityEventsDialog } from "../../../admin-console/organizations/manage/entity-events.component"; import { BasePeopleComponent } from "../../common/base.people.component"; import { GroupService } from "../core"; @@ -74,10 +72,7 @@ import { ResetPasswordComponent } from "./components/reset-password.component"; selector: "app-org-people", templateUrl: "people.component.html", }) -export class PeopleComponent - extends BasePeopleComponent - implements OnInit, OnDestroy -{ +export class PeopleComponent extends BasePeopleComponent { @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef; @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) @@ -100,7 +95,6 @@ export class PeopleComponent orgResetPasswordPolicyEnabled = false; protected canUseSecretsManager$: Observable; - private destroy$ = new Subject(); constructor( apiService: ApiService, @@ -148,9 +142,7 @@ export class PeopleComponent shareReplay({ refCount: true, bufferSize: 1 }), ); - this.canUseSecretsManager$ = organization$.pipe( - map((org) => org.useSecretsManager && flagEnabled("secretsManager")), - ); + this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager)); const policies$ = organization$.pipe( switchMap((organization) => { @@ -213,8 +205,7 @@ export class PeopleComponent } ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + super.ngOnDestroy(); } async load() { @@ -571,7 +562,8 @@ export class PeopleComponent } async bulkEnableSM() { - const users = this.getCheckedUsers(); + const users = this.getCheckedUsers().filter((ou) => !ou.accessSecretsManager); + if (users.length === 0) { this.platformUtilsService.showToast( "error", @@ -588,6 +580,7 @@ export class PeopleComponent await lastValueFrom(dialogRef.closed); this.selectAll(false); + await this.load(); } async events(user: OrganizationUserView) { diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index 7035b976ca..082fe7eb80 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -20,10 +20,6 @@ {{ "billingEmail" | i18n }} - - {{ "businessName" | i18n }} - -
diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 8527aa1b17..1ce05f7a30 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -11,7 +11,7 @@ import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/ import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request"; import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -65,10 +65,6 @@ export class AccountComponent { { value: "", disabled: true }, { validators: [Validators.required, Validators.email, Validators.maxLength(256)] }, ), - businessName: this.formBuilder.control( - { value: "", disabled: true }, - { validators: [Validators.maxLength(50)] }, - ), }); protected collectionManagementFormGroup = this.formBuilder.group({ @@ -95,7 +91,7 @@ export class AccountComponent { private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private formBuilder: FormBuilder, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit() { @@ -124,7 +120,6 @@ export class AccountComponent { // Update disabled states - reactive forms prefers not using disabled attribute if (!this.selfHosted) { this.formGroup.get("orgName").enable(); - this.formGroup.get("businessName").enable(); this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable(); this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable(); } @@ -143,7 +138,6 @@ export class AccountComponent { this.formGroup.patchValue({ orgName: this.org.name, billingEmail: this.org.billingEmail, - businessName: this.org.businessName, }); this.collectionManagementFormGroup.patchValue({ limitCollectionCreationDeletion: this.org.limitCollectionCreationDeletion, @@ -168,7 +162,6 @@ export class AccountComponent { const request = new OrganizationUpdateRequest(); request.name = this.formGroup.value.orgName; - request.businessName = this.formGroup.value.businessName; request.billingEmail = this.formGroup.value.billingEmail; // Backfill pub/priv key if necessary diff --git a/apps/web/src/app/admin-console/organizations/settings/settings.component.html b/apps/web/src/app/admin-console/organizations/settings/settings.component.html deleted file mode 100644 index 47592df378..0000000000 --- a/apps/web/src/app/admin-console/organizations/settings/settings.component.html +++ /dev/null @@ -1,88 +0,0 @@ - diff --git a/apps/web/src/app/admin-console/organizations/settings/settings.component.ts b/apps/web/src/app/admin-console/organizations/settings/settings.component.ts deleted file mode 100644 index ab25829d19..0000000000 --- a/apps/web/src/app/admin-console/organizations/settings/settings.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { Observable, switchMap } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - -@Component({ - selector: "app-org-settings", - templateUrl: "settings.component.html", -}) -export class SettingsComponent implements OnInit { - organization$: Observable; - FeatureFlag = FeatureFlag; - - constructor( - private route: ActivatedRoute, - private organizationService: OrganizationService, - ) {} - - ngOnInit() { - this.organization$ = this.route.params.pipe( - switchMap((params) => this.organizationService.get$(params.organizationId)), - ); - } -} diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html index 16a24781df..f4c9c840ef 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html @@ -22,7 +22,7 @@ - + {{ selectorLabelText }}
@@ -139,7 +139,7 @@ - - - - diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index 4cbdbf3864..b228a4d135 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -1,12 +1,7 @@ -import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; -import { Component, Inject } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; - +import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { Verification } from "@bitwarden/common/auth/types/verification"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -19,63 +14,58 @@ interface EnrollMasterPasswordResetData { organization: Organization; } -@Component({ - selector: "app-enroll-master-password-reset", - templateUrl: "enroll-master-password-reset.component.html", -}) export class EnrollMasterPasswordReset { - protected organization: Organization; + constructor() {} - protected formGroup = new FormGroup({ - verification: new FormControl(null, Validators.required), - }); - - constructor( - private dialogRef: DialogRef, - @Inject(DIALOG_DATA) protected data: EnrollMasterPasswordResetData, - private resetPasswordService: OrganizationUserResetPasswordService, - private userVerificationService: UserVerificationService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private syncService: SyncService, - private logService: LogService, - private organizationUserService: OrganizationUserService, + static async open( + dialogService: DialogService, + data: EnrollMasterPasswordResetData, + resetPasswordService: OrganizationUserResetPasswordService, + organizationUserService: OrganizationUserService, + platformUtilsService: PlatformUtilsService, + i18nService: I18nService, + syncService: SyncService, + logService: LogService, ) { - this.organization = data.organization; - } + const result = await UserVerificationDialogComponent.open(dialogService, { + title: "enrollAccountRecovery", + calloutOptions: { + text: "resetPasswordEnrollmentWarning", + type: "warning", + }, + }); - submit = async () => { - try { - await this.userVerificationService - .buildRequest( - this.formGroup.value.verification, - OrganizationUserResetPasswordEnrollmentRequest, - ) - .then(async (request) => { - // Create request and execute enrollment - request.resetPasswordKey = await this.resetPasswordService.buildRecoveryKey( - this.organization.id, - ); - await this.organizationUserService.putOrganizationUserResetPasswordEnrollment( - this.organization.id, - this.organization.userId, - request, - ); - - await this.syncService.fullSync(true); - }); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("enrollPasswordResetSuccess"), - ); - this.dialogRef.close(); - } catch (e) { - this.logService.error(e); + // Handle the result of the dialog based on user action and verification success + if (result.userAction === "cancel") { + return; } - }; - static open(dialogService: DialogService, data: EnrollMasterPasswordResetData) { - return dialogService.open(EnrollMasterPasswordReset, { data }); + // User confirmed the dialog so check verification success + if (!result.verificationSuccess) { + // verification failed + return; + } + + // Verification succeeded + try { + // This object is missing most of the properties in the + // `OrganizationUserResetPasswordEnrollmentRequest()`, but those + // properties don't carry over to the server model anyway and are + // never used by this flow. + const request = new OrganizationUserResetPasswordEnrollmentRequest(); + request.resetPasswordKey = await resetPasswordService.buildRecoveryKey(data.organization.id); + + await organizationUserService.putOrganizationUserResetPasswordEnrollment( + data.organization.id, + data.organization.userId, + request, + ); + + platformUtilsService.showToast("success", null, i18nService.t("enrollPasswordResetSuccess")); + + await syncService.fullSync(true); + } catch (e) { + logService.error(e); + } } } diff --git a/apps/web/src/app/admin-console/organizations/users/organization-user.module.ts b/apps/web/src/app/admin-console/organizations/users/organization-user.module.ts deleted file mode 100644 index 30e2b5abe7..0000000000 --- a/apps/web/src/app/admin-console/organizations/users/organization-user.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ScrollingModule } from "@angular/cdk/scrolling"; -import { NgModule } from "@angular/core"; - -import { UserVerificationModule } from "../../../auth/shared/components/user-verification"; -import { LooseComponentsModule, SharedModule } from "../../../shared"; - -import { EnrollMasterPasswordReset } from "./enroll-master-password-reset.component"; - -@NgModule({ - imports: [SharedModule, ScrollingModule, LooseComponentsModule, UserVerificationModule], - declarations: [EnrollMasterPasswordReset], - exports: [EnrollMasterPasswordReset], -}) -export class OrganizationUserModule {} diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html new file mode 100644 index 0000000000..a287a537a4 --- /dev/null +++ b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.html @@ -0,0 +1,34 @@ +
+
+
+

{{ "deleteProvider" | i18n }}

+
+
+ {{ "deleteProviderWarning" | i18n }} +

+ {{ name }} +

+

{{ "deleteProviderRecoverConfirmDesc" | i18n }}

+
+
+ + + {{ "cancel" | i18n }} + +
+
+
+
+
+
diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts new file mode 100644 index 0000000000..0550820cda --- /dev/null +++ b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-verify-recover-delete.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +@Component({ + selector: "app-verify-recover-delete-provider", + templateUrl: "verify-recover-delete-provider.component.html", +}) +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class VerifyRecoverDeleteProviderComponent implements OnInit { + name: string; + formPromise: Promise; + + private providerId: string; + private token: string; + + constructor( + private router: Router, + private providerApiService: ProviderApiServiceAbstraction, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private route: ActivatedRoute, + private logService: LogService, + ) {} + + async ngOnInit() { + const qParams = await firstValueFrom(this.route.queryParams); + if (qParams.providerId != null && qParams.token != null && qParams.name != null) { + this.providerId = qParams.providerId; + this.token = qParams.token; + this.name = qParams.name; + } else { + await this.router.navigate(["/"]); + } + } + + async submit() { + try { + const request = new ProviderVerifyRecoverDeleteRequest(this.token); + this.formPromise = this.providerApiService.providerRecoverDeleteToken( + this.providerId, + request, + ); + await this.formPromise; + this.platformUtilsService.showToast( + "success", + this.i18nService.t("providerDeleted"), + this.i18nService.t("providerDeletedDesc"), + ); + await this.router.navigate(["/"]); + } catch (e) { + this.logService.error(e); + } + } +} diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index a1b7456627..7a3b34969a 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -16,7 +16,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -83,7 +83,7 @@ export class AppComponent implements OnDestroy, OnInit { private policyService: InternalPolicyService, protected policyListService: PolicyListService, private keyConnectorService: KeyConnectorService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private dialogService: DialogService, private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, @@ -158,7 +158,7 @@ export class AppComponent implements OnDestroy, OnInit { break; case "syncCompleted": if (message.successfully) { - this.configService.triggerServerConfigFetch(); + await this.configService.ensureConfigFetched(); } break; case "upgradeOrganization": { @@ -274,16 +274,14 @@ export class AppComponent implements OnDestroy, OnInit { this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), - this.policyService.clear(userId), this.passwordGenerationService.clear(), - this.keyConnectorService.clear(), this.biometricStateService.logout(userId as UserId), this.paymentMethodWarningService.clear(), ]); await this.stateEventRunnerService.handleEvent("logout", userId as UserId); - this.searchService.clearIndex(); + await this.searchService.clearIndex(); this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( diff --git a/apps/web/src/app/auth/hint.component.ts b/apps/web/src/app/auth/hint.component.ts index d3a7c00431..116b0f3f83 100644 --- a/apps/web/src/app/auth/hint.component.ts +++ b/apps/web/src/app/auth/hint.component.ts @@ -2,8 +2,8 @@ import { Component } from "@angular/core"; import { Router } from "@angular/router"; import { HintComponent as BaseHintComponent } from "@bitwarden/angular/auth/components/hint.component"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -19,8 +19,8 @@ export class HintComponent extends BaseHintComponent { apiService: ApiService, platformUtilsService: PlatformUtilsService, logService: LogService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginService); + super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); } } diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index 93ee857617..0997f18864 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -2,14 +2,17 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { Send } from "@bitwarden/common/tools/send/models/domain/send"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -19,6 +22,10 @@ import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + FakeAccountService, + mockAccountServiceWith, +} from "../../../../../../libs/common/spec/fake-account-service"; import { OrganizationUserResetPasswordService } from "../../admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service"; import { StateService } from "../../core"; import { EmergencyAccessService } from "../emergency-access"; @@ -39,9 +46,14 @@ describe("KeyRotationService", () => { let mockCryptoService: MockProxy; let mockEncryptService: MockProxy; let mockStateService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; + + const mockUserId = Utils.newGuid() as UserId; + const mockAccountService: FakeAccountService = mockAccountServiceWith(mockUserId); + let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); beforeAll(() => { + mockMasterPasswordService = new FakeMasterPasswordService(); mockApiService = mock(); mockCipherService = mock(); mockFolderService = mock(); @@ -52,9 +64,10 @@ describe("KeyRotationService", () => { mockCryptoService = mock(); mockEncryptService = mock(); mockStateService = mock(); - mockConfigService = mock(); + mockConfigService = mock(); keyRotationService = new UserKeyRotationService( + mockMasterPasswordService, mockApiService, mockCipherService, mockFolderService, @@ -65,6 +78,7 @@ describe("KeyRotationService", () => { mockCryptoService, mockEncryptService, mockStateService, + mockAccountService, mockConfigService, ); }); @@ -167,7 +181,10 @@ describe("KeyRotationService", () => { it("saves the master key in state after creation", async () => { await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword"); - expect(mockCryptoService.setMasterKey).toHaveBeenCalledWith("mockMasterKey" as any); + expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + "mockMasterKey" as any, + mockUserId, + ); }); it("uses legacy rotation if feature flag is off", async () => { diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index bb4c3494dd..f5812d341a 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -1,9 +1,11 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -24,6 +26,7 @@ import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; @Injectable() export class UserKeyRotationService { constructor( + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private apiService: UserKeyRotationApiService, private cipherService: CipherService, private folderService: FolderService, @@ -34,7 +37,8 @@ export class UserKeyRotationService { private cryptoService: CryptoService, private encryptService: EncryptService, private stateService: StateService, - private configService: ConfigServiceAbstraction, + private accountService: AccountService, + private configService: ConfigService, ) {} /** @@ -59,7 +63,8 @@ export class UserKeyRotationService { } // Set master key again in case it was lost (could be lost on refresh) - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey); if (!newUserKey || !newEncUserKey) { @@ -90,7 +95,12 @@ export class UserKeyRotationService { await this.rotateUserKeyAndEncryptedDataLegacy(request); } - await this.deviceTrustCryptoService.rotateDevicesTrust(newUserKey, masterPasswordHash); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.deviceTrustCryptoService.rotateDevicesTrust( + activeAccount.id, + newUserKey, + masterPasswordHash, + ); } private async encryptPrivateKey(newUserKey: UserKey): Promise { diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index c4f8d276bb..021bf0f9df 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,77 +1,12 @@ -import { Component, NgZone } from "@angular/core"; -import { Router } from "@angular/router"; +import { Component } from "@angular/core"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; -import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; -import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { DialogService } from "@bitwarden/components"; @Component({ selector: "app-lock", templateUrl: "lock.component.html", }) export class LockComponent extends BaseLockComponent { - constructor( - router: Router, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - messagingService: MessagingService, - cryptoService: CryptoService, - vaultTimeoutService: VaultTimeoutService, - vaultTimeoutSettingsService: VaultTimeoutSettingsService, - environmentService: EnvironmentService, - stateService: StateService, - apiService: ApiService, - logService: LogService, - ngZone: NgZone, - policyApiService: PolicyApiServiceAbstraction, - policyService: InternalPolicyService, - passwordStrengthService: PasswordStrengthServiceAbstraction, - dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, - userVerificationService: UserVerificationService, - pinCryptoService: PinCryptoServiceAbstraction, - biometricStateService: BiometricStateService, - ) { - super( - router, - i18nService, - platformUtilsService, - messagingService, - cryptoService, - vaultTimeoutService, - vaultTimeoutSettingsService, - environmentService, - stateService, - apiService, - logService, - ngZone, - policyApiService, - policyService, - passwordStrengthService, - dialogService, - deviceTrustCryptoService, - userVerificationService, - pinCryptoService, - biometricStateService, - ); - } - async ngOnInit() { await super.ngOnInit(); this.onSuccessfulSubmit = async () => { diff --git a/apps/web/src/app/auth/login/login-via-auth-request.component.ts b/apps/web/src/app/auth/login/login-via-auth-request.component.ts index a3bf1160a3..5bca718304 100644 --- a/apps/web/src/app/auth/login/login-via-auth-request.component.ts +++ b/apps/web/src/app/auth/login/login-via-auth-request.component.ts @@ -1,75 +1,9 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; +import { Component } from "@angular/core"; import { LoginViaAuthRequestComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/auth/components/login-via-auth-request.component"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, -} from "@bitwarden/auth/common"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; -import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; - -import { StateService } from "../../core"; @Component({ selector: "app-login-via-auth-request", templateUrl: "login-via-auth-request.component.html", }) -export class LoginViaAuthRequestComponent - extends BaseLoginWithDeviceComponent - implements OnInit, OnDestroy -{ - constructor( - router: Router, - cryptoService: CryptoService, - cryptoFunctionService: CryptoFunctionService, - appIdService: AppIdService, - passwordGenerationService: PasswordGenerationServiceAbstraction, - apiService: ApiService, - authService: AuthService, - logService: LogService, - environmentService: EnvironmentService, - i18nService: I18nService, - platformUtilsService: PlatformUtilsService, - anonymousHubService: AnonymousHubService, - validationService: ValidationService, - stateService: StateService, - loginService: LoginService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, - authRequestService: AuthRequestServiceAbstraction, - loginStrategyService: LoginStrategyServiceAbstraction, - ) { - super( - router, - cryptoService, - cryptoFunctionService, - appIdService, - passwordGenerationService, - apiService, - authService, - logService, - environmentService, - i18nService, - platformUtilsService, - anonymousHubService, - validationService, - stateService, - loginService, - deviceTrustCryptoService, - authRequestService, - loginStrategyService, - ); - } -} +export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent {} diff --git a/apps/web/src/app/auth/login/login.component.html b/apps/web/src/app/auth/login/login.component.html index 5c68058a3c..0e29a34278 100644 --- a/apps/web/src/app/auth/login/login.component.html +++ b/apps/web/src/app/auth/login/login.component.html @@ -1,6 +1,6 @@
{{ "getMasterPasswordHint" | i18n }}
diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index 1d2d1859e9..9f628b9389 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -6,7 +6,10 @@ import { first } from "rxjs/operators"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, +} from "@bitwarden/auth/common"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data"; @@ -14,7 +17,6 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -62,7 +64,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { private routerService: RouterService, formBuilder: FormBuilder, formValidationErrorService: FormValidationErrorsService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -82,7 +84,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { formBuilder, formValidationErrorService, route, - loginService, + loginEmailService, ssoLoginService, webAuthnLoginService, ); @@ -173,14 +175,14 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { } } - this.loginService.clearValues(); + this.loginEmailService.clearValues(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate([this.successRoute]); } goToHint() { - this.setFormValues(); + this.setLoginEmailValues(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigateByUrl("/hint"); @@ -201,15 +203,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { this.router.navigate(["/register"]); } - async submit() { - const rememberEmail = this.formGroup.value.rememberEmail; - - if (!rememberEmail) { - await this.stateService.setRememberedEmail(null); - } - await super.submit(false); - } - protected override handleMigrateEncryptionKey(result: AuthResult): boolean { if (!result.requiresEncryptionKeyMigration) { return false; diff --git a/apps/web/src/app/auth/settings/account/change-avatar.component.html b/apps/web/src/app/auth/settings/account/change-avatar.component.html index 38e35399ae..3a974241d5 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar.component.html +++ b/apps/web/src/app/auth/settings/account/change-avatar.component.html @@ -43,10 +43,10 @@ (click)="showCustomPicker()" title="{{ 'customColor' | i18n }}" [ngClass]="{ - '!tw-outline-[3px] tw-outline-primary-500 hover:tw-outline-[3px] hover:tw-outline-primary-500': + '!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600': customColorSelected }" - class="tw-outline-solid tw-bg-white tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-500 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-500" + class="tw-outline-solid tw-bg-white tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" [style.background-color]="customColor$ | async" > {{ "dangerZone" | i18n }}

-
+

{{ "dangerZoneDesc" | i18n }}

diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts index d20f0cd1bd..9312ce5fc0 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-add-edit-cipher.component.ts @@ -6,7 +6,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -52,7 +52,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent { sendApiService: SendApiService, dialogService: DialogService, datePipe: DatePipe, - configService: ConfigServiceAbstraction, + configService: ConfigService, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( diff --git a/apps/web/src/app/auth/settings/verify-email.component.html b/apps/web/src/app/auth/settings/verify-email.component.html index 6fd2128651..ccad78348c 100644 --- a/apps/web/src/app/auth/settings/verify-email.component.html +++ b/apps/web/src/app/auth/settings/verify-email.component.html @@ -1,5 +1,5 @@ -
-
+
+
{{ "verifyEmail" | i18n }}
diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts index 39a2389c5a..65902a64c9 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey-failed.icon.ts @@ -2,27 +2,27 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyFailedIcon = svgIcon` - - - - - - - - - `; diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts index c0e984bbee..79ba4021b5 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-passkey.icon.ts @@ -2,25 +2,25 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyIcon = svgIcon` - - - - - - - - `; diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 2ef4f3eb15..e120b2749f 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -10,10 +10,12 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -45,7 +47,9 @@ export class SsoComponent extends BaseSsoComponent { private orgDomainApiService: OrgDomainApiServiceAbstraction, private validationService: ValidationService, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, ) { super( ssoLoginService, @@ -62,6 +66,8 @@ export class SsoComponent extends BaseSsoComponent { logService, userDecryptionOptionsService, configService, + masterPasswordService, + accountService, ); this.redirectUri = window.location.origin + "/sso-connector.html"; this.clientId = "web"; diff --git a/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html b/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html index 622a9f6d92..06b1dc7c51 100644 --- a/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html +++ b/apps/web/src/app/auth/trial-initiation/vertical-stepper/vertical-step-content.component.html @@ -14,7 +14,7 @@ class="tw-mr-3.5 tw-w-9 tw-rounded-full tw-font-bold tw-leading-9" *ngIf="!step.completed" [ngClass]="{ - 'tw-bg-primary-500 tw-text-contrast': selected, + 'tw-bg-primary-600 tw-text-contrast': selected, 'tw-bg-secondary-300 tw-text-main': !selected && !disabled && step.editable, 'tw-bg-transparent tw-text-muted': disabled }" @@ -22,7 +22,7 @@ {{ stepNumber }} diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index a47a7a2848..eed84b91f1 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -6,16 +6,18 @@ import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -46,10 +48,12 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - loginService: LoginService, + loginEmailService: LoginEmailServiceAbstraction, userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, @Inject(WINDOW) protected win: Window, ) { super( @@ -65,10 +69,12 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest logService, twoFactorService, appIdService, - loginService, + loginEmailService, userDecryptionOptionsService, ssoLoginService, configService, + masterPasswordService, + accountService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } @@ -103,7 +109,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest } goAfterLogIn = async () => { - this.loginService.clearValues(); + this.loginEmailService.clearValues(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate([this.successRoute], { diff --git a/apps/web/src/app/billing/individual/billing-history-view.component.html b/apps/web/src/app/billing/individual/billing-history-view.component.html index 1032558f5f..2491fc42c7 100644 --- a/apps/web/src/app/billing/individual/billing-history-view.component.html +++ b/apps/web/src/app/billing/individual/billing-history-view.component.html @@ -1,13 +1,12 @@ -
-

+
+

{{ "billingHistory" | i18n }} -

+

- {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium.component.ts index fa17821d56..60536c17b5 100644 --- a/apps/web/src/app/billing/individual/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium.component.ts @@ -124,10 +124,7 @@ export class PremiumComponent implements OnInit { await this.apiService.refreshIdentityToken(); await this.syncService.fullSync(true); this.platformUtilsService.showToast("success", null, this.i18nService.t("premiumUpdated")); - this.messagingService.send("purchasedPremium"); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/settings/subscription/user-subscription"]); + await this.router.navigate(["/settings/subscription/user-subscription"]); } get additionalStorageTotal(): number { diff --git a/apps/web/src/app/billing/individual/user-subscription.component.html b/apps/web/src/app/billing/individual/user-subscription.component.html index 874983df84..380116e81b 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.html +++ b/apps/web/src/app/billing/individual/user-subscription.component.html @@ -170,8 +170,8 @@
-
-
-
diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 7d8c3a0f18..fa21317c18 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -12,6 +12,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustStorageDialogResult, + openAdjustStorageDialog, +} from "../shared/adjust-storage.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -24,7 +28,6 @@ export class UserSubscriptionComponent implements OnInit { loading = false; firstLoaded = false; adjustStorageAdd = true; - showAdjustStorage = false; showUpdateLicense = false; sub: SubscriptionResponse; selfHosted = false; @@ -144,19 +147,20 @@ export class UserSubscriptionComponent implements OnInit { } } - adjustStorage(add: boolean) { - this.adjustStorageAdd = add; - this.showAdjustStorage = true; - } - - closeStorage(load: boolean) { - this.showAdjustStorage = false; - if (load) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } - } + adjustStorage = (add: boolean) => { + return async () => { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: 4, + add: add, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } + }; + }; get subscriptionMarkedForCancel() { return ( diff --git a/apps/web/src/app/billing/organizations/change-plan.component.html b/apps/web/src/app/billing/organizations/change-plan.component.html index b9a15be5ea..a25dde4fd3 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.html +++ b/apps/web/src/app/billing/organizations/change-plan.component.html @@ -1,10 +1,18 @@ -
-
- -

{{ "changeBillingPlan" | i18n }}

-

{{ "changeBillingPlanUpgrade" | i18n }}

+
+
+ +

{{ "changeBillingPlan" | i18n }}

+

{{ "changeBillingPlanUpgrade" | i18n }}

- {{ "loading" | i18n }} + {{ "loading" | i18n }} diff --git a/apps/web/src/app/billing/organizations/organization-billing-tab.component.ts b/apps/web/src/app/billing/organizations/organization-billing-tab.component.ts deleted file mode 100644 index 25d4ea4892..0000000000 --- a/apps/web/src/app/billing/organizations/organization-billing-tab.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { map, Observable, switchMap } from "rxjs"; - -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -@Component({ - templateUrl: "organization-billing-tab.component.html", -}) -export class OrganizationBillingTabComponent implements OnInit { - showPaymentAndHistory$: Observable; - - constructor( - private route: ActivatedRoute, - private organizationService: OrganizationService, - private platformUtilsService: PlatformUtilsService, - ) {} - - ngOnInit() { - this.showPaymentAndHistory$ = this.route.params.pipe( - switchMap((params) => this.organizationService.get$(params.organizationId)), - map( - (org) => - !this.platformUtilsService.isSelfHost() && - org.canViewBillingHistory && - org.canEditPaymentMethods, - ), - ); - } -} diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index 141233aef3..490ebafbff 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -17,6 +17,7 @@ import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscr import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component"; import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component"; import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; +import { SubscriptionStatusComponent } from "./subscription-status.component"; @NgModule({ imports: [ @@ -38,6 +39,7 @@ import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; SecretsManagerAdjustSubscriptionComponent, SecretsManagerSubscribeStandaloneComponent, SubscriptionHiddenComponent, + SubscriptionStatusComponent, ], }) export class OrganizationBillingModule {} diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 1242018673..30691ce87d 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -15,6 +15,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationCreateRequest } from "@bitwarden/common/admin-console/models/request/organization-create.request"; @@ -47,7 +48,11 @@ interface OnSuccessArgs { organizationId: string; } -const Allowed2020PlanTypes = [ +const AllowedLegacyPlanTypes = [ + PlanType.TeamsMonthly2023, + PlanType.TeamsAnnually2023, + PlanType.EnterpriseAnnually2023, + PlanType.EnterpriseMonthly2023, PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, PlanType.EnterpriseAnnually2020, @@ -116,7 +121,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { additionalStorage: [0, [Validators.min(0), Validators.max(99)]], additionalSeats: [0, [Validators.min(0), Validators.max(100000)]], clientOwnerEmail: ["", [Validators.email]], - businessName: [""], plan: [this.plan], product: [this.product], secretsManager: this.secretsManagerSubscription, @@ -144,6 +148,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private messagingService: MessagingService, private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, + private providerApiService: ProviderApiServiceAbstraction, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -179,7 +184,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.hasProvider) { this.formGroup.controls.businessOwned.setValue(true); this.changedOwnedBusiness(); - this.provider = await this.apiService.getProvider(this.providerId); + this.provider = await this.providerApiService.getProvider(this.providerId); const providerDefaultPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.TeamsAnnually, ); @@ -278,7 +283,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) && (!this.hasProvider || plan.product !== ProductType.TeamsStarter) && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && Allowed2020PlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && AllowedLegacyPlanTypes.includes(plan.type))), ); result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -293,7 +298,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (plan) => plan.product === selectedProductType && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && Allowed2020PlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && AllowedLegacyPlanTypes.includes(plan.type))), ) || []; result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -592,9 +597,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private async updateOrganization(orgId: string) { const request = new OrganizationUpgradeRequest(); - request.businessName = this.formGroup.controls.businessOwned.value - ? this.formGroup.controls.businessName.value - : null; request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = @@ -652,9 +654,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { request.paymentToken = tokenResult[0]; request.paymentMethodType = tokenResult[1]; - request.businessName = this.formGroup.controls.businessOwned.value - ? this.formGroup.controls.businessName.value - : null; request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 7f53fba1c0..16641c0d52 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -12,45 +12,58 @@ > - - {{ "subscriptionCanceled" | i18n }} - -

{{ "subscriptionPendingCanceled" | i18n }}

- -
+ + + {{ "subscriptionCanceled" | i18n }} + +

{{ "subscriptionPendingCanceled" | i18n }}

+ +
-
-
{{ "billingPlan" | i18n }}
-
{{ sub.plan.name }}
- -
{{ "status" | i18n }}
-
- {{ - isSponsoredSubscription ? "sponsored" : subscription.status || "-" - }} - {{ - "pendingCancellation" | i18n - }} -
-
- {{ "subscriptionExpiration" | i18n }} -
-
- {{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }} -
-
-
+
+
{{ "billingPlan" | i18n }}
+
{{ sub.plan.name }}
+ +
{{ "status" | i18n }}
+
+ {{ + isSponsoredSubscription ? "sponsored" : subscription.status || "-" + }} + {{ + "pendingCancellation" | i18n + }} +
+
+ {{ "subscriptionExpiration" | i18n }} +
+
+ {{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }} +
+
+
+
+
{{ @@ -162,23 +175,24 @@
-
- -
-
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index 2256a92756..9326359bd8 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -11,11 +11,17 @@ import { PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { BillingSubscriptionItemResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { ProductType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustStorageDialogResult, + openAdjustStorageDialog, +} from "../shared/adjust-storage.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, @@ -34,13 +40,13 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy userOrg: Organization; showChangePlan = false; showDownloadLicense = false; - adjustStorageAdd = true; - showAdjustStorage = false; hasBillingSyncToken: boolean; showAdjustSecretsManager = false; showSecretsManagerSubscribe = false; firstLoaded = false; loading: boolean; + locale: string; + showUpdatedSubscriptionStatusSection$: Observable; protected readonly teamsStarter = ProductType.TeamsStarter; @@ -55,6 +61,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private organizationApiService: OrganizationApiServiceAbstraction, private route: ActivatedRoute, private dialogService: DialogService, + private configService: ConfigService, ) {} async ngOnInit() { @@ -74,6 +81,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy takeUntil(this.destroy$), ) .subscribe(); + + this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( + FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, + false, + ); } ngOnDestroy() { @@ -86,6 +98,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return; } this.loading = true; + this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); @@ -230,6 +243,8 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy return ( this.sub.planType === PlanType.EnterpriseAnnually || this.sub.planType === PlanType.EnterpriseMonthly || + this.sub.planType === PlanType.EnterpriseAnnually2023 || + this.sub.planType === PlanType.EnterpriseMonthly2023 || this.sub.planType === PlanType.EnterpriseAnnually2020 || this.sub.planType === PlanType.EnterpriseMonthly2020 || this.sub.planType === PlanType.EnterpriseAnnually2019 || @@ -243,6 +258,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } else if ( this.sub.planType === PlanType.FamiliesAnnually || this.sub.planType === PlanType.FamiliesAnnually2019 || + this.sub.planType === PlanType.TeamsStarter2023 || this.sub.planType === PlanType.TeamsStarter ) { if (this.isSponsoredSubscription) { @@ -347,19 +363,22 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.load(); } - adjustStorage(add: boolean) { - this.adjustStorageAdd = add; - this.showAdjustStorage = true; - } - - closeStorage(load: boolean) { - this.showAdjustStorage = false; - if (load) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); - } - } + adjustStorage = (add: boolean) => { + return async () => { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: this.storageGbPrice, + add: add, + organizationId: this.organizationId, + interval: this.billingInterval, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } + }; + }; removeSponsorship = async () => { const confirmed = await this.dialogService.openSimpleDialog({ diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html index c10ef9af28..4c8ea28284 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.html @@ -33,7 +33,7 @@ - {{ "additionalServiceAccounts" | i18n }} + {{ "additionalMachineAccounts" | i18n }}
- {{ "includedServiceAccounts" | i18n: options.baseServiceAccountCount }} - {{ "addAdditionalServiceAccounts" | i18n: (monthlyServiceAccountPrice | currency: "$") }} + {{ "includedMachineAccounts" | i18n: options.baseServiceAccountCount }} + {{ "addAdditionalMachineAccounts" | i18n: (monthlyServiceAccountPrice | currency: "$") }}
{{ "total" | i18n }}: @@ -56,7 +56,7 @@ - {{ "limitServiceAccounts" | i18n }} + {{ "limitMachineAccounts" | i18n }} - {{ "limitServiceAccountsDesc" | i18n }} + {{ "limitMachineAccountsDesc" | i18n }} - {{ "serviceAccountLimit" | i18n }} + {{ "machineAccountLimit" | i18n }}
- {{ "includedServiceAccounts" | i18n: options.baseServiceAccountCount }} + {{ "includedMachineAccounts" | i18n: options.baseServiceAccountCount }}
- {{ "maxServiceAccountCost" | i18n }}: + {{ "maxMachineAccountCost" | i18n }}: {{ maxAdditionalServiceAccounts }} × {{ options.additionalServiceAccountPrice | currency: "$" }} = {{ maxServiceAccountTotalCost | currency: "$" }} / {{ options.interval | i18n }} diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.html b/apps/web/src/app/billing/organizations/subscription-status.component.html new file mode 100644 index 0000000000..99673a228e --- /dev/null +++ b/apps/web/src/app/billing/organizations/subscription-status.component.html @@ -0,0 +1,32 @@ + + +

{{ data.callout.body }}

+ +
+
+
{{ "billingPlan" | i18n }}
+
{{ planName }}
+ +
{{ data.status.label }}
+
+ + {{ displayedStatus }} + +
+
+ {{ data.date.label | titlecase }} +
+
+ {{ data.date.value | date: "mediumDate" }} +
+
+
+
diff --git a/apps/web/src/app/billing/organizations/subscription-status.component.ts b/apps/web/src/app/billing/organizations/subscription-status.component.ts new file mode 100644 index 0000000000..9a0b634edc --- /dev/null +++ b/apps/web/src/app/billing/organizations/subscription-status.component.ts @@ -0,0 +1,189 @@ +import { DatePipe } from "@angular/common"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +type ComponentData = { + status?: { + label: string; + value: string; + }; + date?: { + label: string; + value: string; + }; + callout?: { + severity: "danger" | "warning"; + header: string; + body: string; + showReinstatementButton: boolean; + }; +}; + +@Component({ + selector: "app-subscription-status", + templateUrl: "subscription-status.component.html", +}) +export class SubscriptionStatusComponent { + @Input({ required: true }) organizationSubscriptionResponse: OrganizationSubscriptionResponse; + @Output() reinstatementRequested = new EventEmitter(); + + constructor( + private datePipe: DatePipe, + private i18nService: I18nService, + ) {} + + get displayedStatus(): string { + const sponsored = this.subscription.items.some((item) => item.sponsoredSubscriptionItem); + return sponsored ? this.i18nService.t("sponsored") : this.data.status.value; + } + + get planName() { + return this.organizationSubscriptionResponse.plan.name; + } + + get status(): string { + return this.subscription + ? this.subscription.status != "canceled" && this.subscription.cancelAtEndDate + ? "pending_cancellation" + : this.subscription.status + : "free"; + } + + get subscription() { + return this.organizationSubscriptionResponse?.subscription; + } + + get data(): ComponentData { + const defaultStatusLabel = this.i18nService.t("status"); + + const nextChargeDateLabel = this.i18nService.t("nextCharge"); + const subscriptionExpiredDateLabel = this.i18nService.t("subscriptionExpired"); + const cancellationDateLabel = this.i18nService.t("cancellationDate"); + + switch (this.status) { + case "free": { + return {}; + } + case "trialing": { + return { + status: { + label: defaultStatusLabel, + value: this.i18nService.t("trial"), + }, + date: { + label: nextChargeDateLabel, + value: this.subscription.periodEndDate, + }, + }; + } + case "active": { + return { + status: { + label: defaultStatusLabel, + value: this.i18nService.t("active"), + }, + date: { + label: nextChargeDateLabel, + value: this.subscription.periodEndDate, + }, + }; + } + case "past_due": { + const pastDueText = this.i18nService.t("pastDue"); + const suspensionDate = this.datePipe.transform( + this.subscription.suspensionDate, + "mediumDate", + ); + const calloutBody = + this.subscription.collectionMethod === "charge_automatically" + ? this.i18nService.t( + "pastDueWarningForChargeAutomatically", + this.subscription.gracePeriod, + suspensionDate, + ) + : this.i18nService.t( + "pastDueWarningForSendInvoice", + this.subscription.gracePeriod, + suspensionDate, + ); + return { + status: { + label: defaultStatusLabel, + value: pastDueText, + }, + date: { + label: subscriptionExpiredDateLabel, + value: this.subscription.unpaidPeriodEndDate, + }, + callout: { + severity: "warning", + header: pastDueText, + body: calloutBody, + showReinstatementButton: false, + }, + }; + } + case "unpaid": { + return { + status: { + label: defaultStatusLabel, + value: this.i18nService.t("unpaid"), + }, + date: { + label: subscriptionExpiredDateLabel, + value: this.subscription.unpaidPeriodEndDate, + }, + callout: { + severity: "danger", + header: this.i18nService.t("unpaidInvoice"), + body: this.i18nService.t("toReactivateYourSubscription"), + showReinstatementButton: false, + }, + }; + } + case "pending_cancellation": { + const pendingCancellationText = this.i18nService.t("pendingCancellation"); + return { + status: { + label: defaultStatusLabel, + value: pendingCancellationText, + }, + date: { + label: cancellationDateLabel, + value: this.subscription.periodEndDate, + }, + callout: { + severity: "warning", + header: pendingCancellationText, + body: this.i18nService.t("subscriptionPendingCanceled"), + showReinstatementButton: true, + }, + }; + } + case "incomplete_expired": + case "canceled": { + const canceledText = this.i18nService.t("canceled"); + return { + status: { + label: defaultStatusLabel, + value: canceledText, + }, + date: { + label: cancellationDateLabel, + value: this.subscription.periodEndDate, + }, + callout: { + severity: "danger", + header: canceledText, + body: this.i18nService.t("subscriptionCanceled"), + showReinstatementButton: false, + }, + }; + } + } + } + + requestReinstatement = () => this.reinstatementRequested.emit(); +} diff --git a/apps/web/src/app/billing/shared/add-credit.component.ts b/apps/web/src/app/billing/shared/add-credit.component.ts index 25d49fac9e..71050a9a6e 100644 --- a/apps/web/src/app/billing/shared/add-credit.component.ts +++ b/apps/web/src/app/billing/shared/add-credit.component.ts @@ -13,7 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PaymentMethodType } from "@bitwarden/common/billing/enums"; import { BitPayInvoiceRequest } from "@bitwarden/common/billing/models/request/bit-pay-invoice.request"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -57,7 +57,7 @@ export class AddCreditComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private organizationService: OrganizationService, private logService: LogService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) { const payPalConfig = process.env.PAYPAL_CONFIG as PayPalConfig; this.ppButtonFormAction = payPalConfig.buttonAction; diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html new file mode 100644 index 0000000000..0f92b023b1 --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts new file mode 100644 index 0000000000..41d0ad7e7a --- /dev/null +++ b/apps/web/src/app/billing/shared/adjust-payment-dialog.component.ts @@ -0,0 +1,110 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, ViewChild } from "@angular/core"; +import { FormGroup } from "@angular/forms"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService } from "@bitwarden/components"; + +import { PaymentComponent } from "./payment.component"; +import { TaxInfoComponent } from "./tax-info.component"; + +export interface AdjustPaymentDialogData { + organizationId: string; + currentType: PaymentMethodType; +} + +export enum AdjustPaymentDialogResult { + Adjusted = "adjusted", + Cancelled = "cancelled", +} + +@Component({ + templateUrl: "adjust-payment-dialog.component.html", +}) +export class AdjustPaymentDialogComponent { + @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; + @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent; + + organizationId: string; + currentType: PaymentMethodType; + paymentMethodType = PaymentMethodType; + + protected DialogResult = AdjustPaymentDialogResult; + protected formGroup = new FormGroup({}); + + constructor( + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData, + private apiService: ApiService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private logService: LogService, + private organizationApiService: OrganizationApiServiceAbstraction, + private paymentMethodWarningService: PaymentMethodWarningService, + ) { + this.organizationId = data.organizationId; + this.currentType = data.currentType; + } + + submit = async () => { + const request = new PaymentRequest(); + const response = this.paymentComponent.createPaymentToken().then((result) => { + request.paymentToken = result[0]; + request.paymentMethodType = result[1]; + request.postalCode = this.taxInfoComponent.taxInfo.postalCode; + request.country = this.taxInfoComponent.taxInfo.country; + if (this.organizationId == null) { + return this.apiService.postAccountPayment(request); + } else { + request.taxId = this.taxInfoComponent.taxInfo.taxId; + request.state = this.taxInfoComponent.taxInfo.state; + request.line1 = this.taxInfoComponent.taxInfo.line1; + request.line2 = this.taxInfoComponent.taxInfo.line2; + request.city = this.taxInfoComponent.taxInfo.city; + request.state = this.taxInfoComponent.taxInfo.state; + return this.organizationApiService.updatePayment(this.organizationId, request); + } + }); + await response; + if (this.organizationId) { + await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId); + } + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("updatedPaymentMethod"), + ); + this.dialogRef.close(AdjustPaymentDialogResult.Adjusted); + }; + + changeCountry() { + if (this.taxInfoComponent.taxInfo.country === "US") { + this.paymentComponent.hideBank = !this.organizationId; + } else { + this.paymentComponent.hideBank = true; + if (this.paymentComponent.method === PaymentMethodType.BankAccount) { + this.paymentComponent.method = PaymentMethodType.Card; + this.paymentComponent.changeMethod(); + } + } + } +} + +/** + * Strongly typed helper to open a AdjustPaymentDialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export function openAdjustPaymentDialog( + dialogService: DialogService, + config: DialogConfig, +) { + return dialogService.open(AdjustPaymentDialogComponent, config); +} diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.html b/apps/web/src/app/billing/shared/adjust-payment.component.html deleted file mode 100644 index 724e7a44c2..0000000000 --- a/apps/web/src/app/billing/shared/adjust-payment.component.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
- -

- {{ (currentType != null ? "changePaymentMethod" : "addPaymentMethod") | i18n }} -

- - - - -
-
diff --git a/apps/web/src/app/billing/shared/adjust-payment.component.ts b/apps/web/src/app/billing/shared/adjust-payment.component.ts deleted file mode 100644 index 7452344141..0000000000 --- a/apps/web/src/app/billing/shared/adjust-payment.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -import { PaymentComponent } from "./payment.component"; -import { TaxInfoComponent } from "./tax-info.component"; - -@Component({ - selector: "app-adjust-payment", - templateUrl: "adjust-payment.component.html", -}) -export class AdjustPaymentComponent { - @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - @ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent; - - @Input() currentType?: PaymentMethodType; - @Input() organizationId: string; - @Output() onAdjusted = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); - - paymentMethodType = PaymentMethodType; - formPromise: Promise; - - constructor( - private apiService: ApiService, - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private logService: LogService, - private organizationApiService: OrganizationApiServiceAbstraction, - private paymentMethodWarningService: PaymentMethodWarningService, - ) {} - - async submit() { - try { - const request = new PaymentRequest(); - this.formPromise = this.paymentComponent.createPaymentToken().then((result) => { - request.paymentToken = result[0]; - request.paymentMethodType = result[1]; - request.postalCode = this.taxInfoComponent.taxInfo.postalCode; - request.country = this.taxInfoComponent.taxInfo.country; - if (this.organizationId == null) { - return this.apiService.postAccountPayment(request); - } else { - request.taxId = this.taxInfoComponent.taxInfo.taxId; - request.state = this.taxInfoComponent.taxInfo.state; - request.line1 = this.taxInfoComponent.taxInfo.line1; - request.line2 = this.taxInfoComponent.taxInfo.line2; - request.city = this.taxInfoComponent.taxInfo.city; - request.state = this.taxInfoComponent.taxInfo.state; - return this.organizationApiService.updatePayment(this.organizationId, request); - } - }); - await this.formPromise; - if (this.organizationId) { - await this.paymentMethodWarningService.removeSubscriptionRisk(this.organizationId); - } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("updatedPaymentMethod"), - ); - this.onAdjusted.emit(); - } catch (e) { - this.logService.error(e); - } - } - - cancel() { - this.onCanceled.emit(); - } - - changeCountry() { - if (this.taxInfoComponent.taxInfo.country === "US") { - this.paymentComponent.hideBank = !this.organizationId; - } else { - this.paymentComponent.hideBank = true; - if (this.paymentComponent.method === PaymentMethodType.BankAccount) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); - } - } - } -} diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.html b/apps/web/src/app/billing/shared/adjust-storage.component.html index aa6daca335..a597a3ae5e 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.html +++ b/apps/web/src/app/billing/shared/adjust-storage.component.html @@ -1,43 +1,35 @@ -
-
- -

{{ (add ? "addStorage" : "removeStorage") | i18n }}

-
-
- - + + + +

{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}

+
+ + {{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }} + + + {{ "total" | i18n }}: + {{ formGroup.get("storageAdjustment").value || 0 }} GB × + {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ + interval | i18n + }} + +
-
-
- {{ "total" | i18n }}: {{ storageAdjustment || 0 }} GB × - {{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{ - interval | i18n - }} -
- - - - {{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }} - -
+ + + + + + diff --git a/apps/web/src/app/billing/shared/adjust-storage.component.ts b/apps/web/src/app/billing/shared/adjust-storage.component.ts index 25462c2829..fcdbc3437d 100644 --- a/apps/web/src/app/billing/shared/adjust-storage.component.ts +++ b/apps/web/src/app/billing/shared/adjust-storage.component.ts @@ -1,4 +1,6 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, ViewChild } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -8,27 +10,45 @@ import { StorageRequest } from "@bitwarden/common/models/request/storage.request import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService } from "@bitwarden/components"; import { PaymentComponent } from "./payment.component"; +export interface AdjustStorageDialogData { + storageGbPrice: number; + add: boolean; + organizationId?: string; + interval?: string; +} + +export enum AdjustStorageDialogResult { + Adjusted = "adjusted", + Cancelled = "cancelled", +} + @Component({ - selector: "app-adjust-storage", templateUrl: "adjust-storage.component.html", }) export class AdjustStorageComponent { - @Input() storageGbPrice = 0; - @Input() add = true; - @Input() organizationId: string; - @Input() interval = "year"; - @Output() onAdjusted = new EventEmitter(); - @Output() onCanceled = new EventEmitter(); + storageGbPrice: number; + add: boolean; + organizationId: string; + interval: string; @ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent; - storageAdjustment = 0; - formPromise: Promise; + protected DialogResult = AdjustStorageDialogResult; + protected formGroup = new FormGroup({ + storageAdjustment: new FormControl(0, [ + Validators.required, + Validators.min(0), + Validators.max(99), + ]), + }); constructor( + private dialogRef: DialogRef, + @Inject(DIALOG_DATA) protected data: AdjustStorageDialogData, private apiService: ApiService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, @@ -36,69 +56,74 @@ export class AdjustStorageComponent { private activatedRoute: ActivatedRoute, private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, - ) {} + ) { + this.storageGbPrice = data.storageGbPrice; + this.add = data.add; + this.organizationId = data.organizationId; + this.interval = data.interval || "year"; + } - async submit() { - try { - const request = new StorageRequest(); - request.storageGbAdjustment = this.storageAdjustment; - if (!this.add) { - request.storageGbAdjustment *= -1; - } - - let paymentFailed = false; - const action = async () => { - let response: Promise; - if (this.organizationId == null) { - response = this.formPromise = this.apiService.postAccountStorage(request); - } else { - response = this.formPromise = this.organizationApiService.updateStorage( - this.organizationId, - request, - ); - } - const result = await response; - if (result != null && result.paymentIntentClientSecret != null) { - try { - await this.paymentComponent.handleStripeCardPayment( - result.paymentIntentClientSecret, - null, - ); - } catch { - paymentFailed = true; - } - } - }; - this.formPromise = action(); - await this.formPromise; - this.onAdjusted.emit(this.storageAdjustment); - if (paymentFailed) { - this.platformUtilsService.showToast( - "warning", - null, - this.i18nService.t("couldNotChargeCardPayInvoice"), - { timeout: 10000 }, - ); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); - } else { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), - ); - } - } catch (e) { - this.logService.error(e); + submit = async () => { + const request = new StorageRequest(); + request.storageGbAdjustment = this.formGroup.value.storageAdjustment; + if (!this.add) { + request.storageGbAdjustment *= -1; } - } - cancel() { - this.onCanceled.emit(); - } + let paymentFailed = false; + const action = async () => { + let response: Promise; + if (this.organizationId == null) { + response = this.apiService.postAccountStorage(request); + } else { + response = this.organizationApiService.updateStorage(this.organizationId, request); + } + const result = await response; + if (result != null && result.paymentIntentClientSecret != null) { + try { + await this.paymentComponent.handleStripeCardPayment( + result.paymentIntentClientSecret, + null, + ); + } catch { + paymentFailed = true; + } + } + }; + await action(); + this.dialogRef.close(AdjustStorageDialogResult.Adjusted); + if (paymentFailed) { + this.platformUtilsService.showToast( + "warning", + null, + this.i18nService.t("couldNotChargeCardPayInvoice"), + { timeout: 10000 }, + ); + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["../billing"], { relativeTo: this.activatedRoute }); + } else { + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()), + ); + } + }; get adjustedStorageTotal(): number { - return this.storageGbPrice * this.storageAdjustment; + return this.storageGbPrice * this.formGroup.value.storageAdjustment; } } + +/** + * Strongly typed helper to open an AdjustStorageDialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export function openAdjustStorageDialog( + dialogService: DialogService, + config: DialogConfig, +) { + return dialogService.open(AdjustStorageComponent, config); +} diff --git a/apps/web/src/app/billing/shared/billing-history.component.html b/apps/web/src/app/billing/shared/billing-history.component.html index 56a8a990d4..1719a59076 100644 --- a/apps/web/src/app/billing/shared/billing-history.component.html +++ b/apps/web/src/app/billing/shared/billing-history.component.html @@ -1,65 +1,72 @@ -

{{ "invoices" | i18n }}

-

{{ "noInvoices" | i18n }}

- - - - - + + + + + + + + + +

{{ "transactions" | i18n }}

+

+ {{ "noTransactions" | i18n }} +

+ + +
+ + + + - - - - -
{{ i.date | date: "mediumDate" }} - +

{{ "invoices" | i18n }}

+

{{ "noInvoices" | i18n }}

+ + +
{{ i.date | date: "mediumDate" }} + + + + {{ "invoiceNumber" | i18n: i.number }} + {{ i.amount | currency: "$" }} + + + {{ "paid" | i18n }} + + + + {{ "unpaid" | i18n }} + +
{{ t.createdDate | date: "mediumDate" }} + + {{ "chargeNoun" | i18n }} + + {{ "refundNoun" | i18n }} + + + {{ t.details }} + - - - {{ "invoiceNumber" | i18n: i.number }} - {{ i.amount | currency: "$" }} - - - {{ "paid" | i18n }} - - - - {{ "unpaid" | i18n }} - -
-

{{ "transactions" | i18n }}

-

{{ "noTransactions" | i18n }}

- - - - - - - - - -
{{ t.createdDate | date: "mediumDate" }} - - {{ "chargeNoun" | i18n }} - - {{ "refundNoun" | i18n }} - - - {{ t.details }} - - {{ t.amount | currency: "$" }} -
-* {{ "chargesStatement" | i18n: "BITWARDEN" }} + {{ t.amount | currency: "$" }} + + + + + * {{ "chargesStatement" | i18n: "BITWARDEN" }} + diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts index 2f773870aa..65a651b73d 100644 --- a/apps/web/src/app/billing/shared/billing-shared.module.ts +++ b/apps/web/src/app/billing/shared/billing-shared.module.ts @@ -4,7 +4,7 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { SharedModule } from "../../shared"; import { AddCreditComponent } from "./add-credit.component"; -import { AdjustPaymentComponent } from "./adjust-payment.component"; +import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog.component"; import { AdjustStorageComponent } from "./adjust-storage.component"; import { BillingHistoryComponent } from "./billing-history.component"; import { OffboardingSurveyComponent } from "./offboarding-survey.component"; @@ -18,7 +18,7 @@ import { UpdateLicenseComponent } from "./update-license.component"; imports: [SharedModule, PaymentComponent, TaxInfoComponent, HeaderModule], declarations: [ AddCreditComponent, - AdjustPaymentComponent, + AdjustPaymentDialogComponent, AdjustStorageComponent, BillingHistoryComponent, PaymentMethodComponent, diff --git a/apps/web/src/app/billing/shared/payment-method.component.html b/apps/web/src/app/billing/shared/payment-method.component.html index cfe98178b0..5f78294fa6 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.html +++ b/apps/web/src/app/billing/shared/payment-method.component.html @@ -15,7 +15,7 @@
- +

{{ "paymentMethod" | i18n }}

@@ -102,23 +102,9 @@ {{ paymentSource.description }}

- - -

{{ "paymentChargedWithUnpaidSubscription" | i18n }}

{{ "taxInformation" | i18n }}

diff --git a/apps/web/src/app/billing/shared/payment-method.component.ts b/apps/web/src/app/billing/shared/payment-method.component.ts index d2b65968c3..fee97cb912 100644 --- a/apps/web/src/app/billing/shared/payment-method.component.ts +++ b/apps/web/src/app/billing/shared/payment-method.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; +import { lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -14,6 +15,10 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; +import { + AdjustPaymentDialogResult, + openAdjustPaymentDialog, +} from "./adjust-payment-dialog.component"; import { TaxInfoComponent } from "./tax-info.component"; @Component({ @@ -25,7 +30,6 @@ export class PaymentMethodComponent implements OnInit { loading = false; firstLoaded = false; - showAdjustPayment = false; showAddCredit = false; billing: BillingPaymentResponse; org: OrganizationSubscriptionResponse; @@ -120,18 +124,18 @@ export class PaymentMethodComponent implements OnInit { } } - changePayment() { - this.showAdjustPayment = true; - } - - closePayment(load: boolean) { - this.showAdjustPayment = false; - if (load) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.load(); + changePayment = async () => { + const dialogRef = openAdjustPaymentDialog(this.dialogService, { + data: { + organizationId: this.organizationId, + currentType: this.paymentSource !== null ? this.paymentSource.type : null, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustPaymentDialogResult.Adjusted) { + await this.load(); } - } + }; async verifyBank() { if (this.loading || !this.forOrganization) { diff --git a/apps/web/src/app/billing/shared/payment.component.ts b/apps/web/src/app/billing/shared/payment.component.ts index 6e34962cb7..652bed7801 100644 --- a/apps/web/src/app/billing/shared/payment.component.ts +++ b/apps/web/src/app/billing/shared/payment.component.ts @@ -287,7 +287,7 @@ export class PaymentComponent implements OnInit, OnDestroy { )})`; this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`; this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue( - "--color-danger-500", + "--color-danger-600", )})`; }); } diff --git a/apps/web/src/app/billing/shared/sm-subscribe.component.html b/apps/web/src/app/billing/shared/sm-subscribe.component.html index c9243e29a6..6cdaeb9476 100644 --- a/apps/web/src/app/billing/shared/sm-subscribe.component.html +++ b/apps/web/src/app/billing/shared/sm-subscribe.component.html @@ -20,10 +20,10 @@
  • {{ "unlimitedProjects" | i18n }}
  • -
  • {{ "serviceAccountsIncluded" | i18n: serviceAccountsIncluded }}
  • +
  • {{ "machineAccountsIncluded" | i18n: serviceAccountsIncluded }}
  • {{ - "additionalServiceAccountCost" | i18n: (monthlyCostPerServiceAccount | currency: "$") + "additionalMachineAccountCost" | i18n: (monthlyCostPerServiceAccount | currency: "$") }}
  • @@ -54,12 +54,12 @@
    - {{ "additionalServiceAccounts" | i18n }} + {{ "additionalMachineAccounts" | i18n }} - {{ "includedServiceAccounts" | i18n: serviceAccountsIncluded }} + {{ "includedMachineAccounts" | i18n: serviceAccountsIncluded }} {{ - "addAdditionalServiceAccounts" | i18n: (monthlyCostPerServiceAccount | currency: "$") + "addAdditionalMachineAccounts" | i18n: (monthlyCostPerServiceAccount | currency: "$") }} diff --git a/apps/web/src/app/billing/shared/tax-info.component.html b/apps/web/src/app/billing/shared/tax-info.component.html index caf92f4189..30cca550d3 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.html +++ b/apps/web/src/app/billing/shared/tax-info.component.html @@ -279,10 +279,7 @@ />
    -
    +
    -
    +
    - +
    -
    +
    diff --git a/apps/web/src/app/billing/shared/tax-info.component.ts b/apps/web/src/app/billing/shared/tax-info.component.ts index 2046cf44be..a704c86eb5 100644 --- a/apps/web/src/app/billing/shared/tax-info.component.ts +++ b/apps/web/src/app/billing/shared/tax-info.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/organization-tax-info-update.request"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { TaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/tax-info-update.request"; import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; import { TaxRateResponse } from "@bitwarden/common/billing/models/response/tax-rate.response"; @@ -29,6 +29,7 @@ export class TaxInfoComponent { loading = true; organizationId: string; + providerId: string; taxInfo: TaxInfoView = { taxId: null, line1: null, @@ -61,6 +62,12 @@ export class TaxInfoComponent { ) {} async ngOnInit() { + // Provider setup + // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe + this.route.queryParams.subscribe((params) => { + this.providerId = params.providerId; + }); + // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organizationId = params.organizationId; @@ -126,9 +133,25 @@ export class TaxInfoComponent { } } + get showTaxIdCheckbox() { + return ( + (this.organizationId || this.providerId) && + this.taxInfo.country !== "US" && + this.countrySupportsTax(this.taxInfo.country) + ); + } + + get showTaxIdFields() { + return ( + (this.organizationId || this.providerId) && + this.taxInfo.includeTaxId && + this.countrySupportsTax(this.taxInfo.country) + ); + } + getTaxInfoRequest(): TaxInfoUpdateRequest { - if (this.organizationId) { - const request = new OrganizationTaxInfoUpdateRequest(); + if (this.organizationId || this.providerId) { + const request = new ExpandedTaxInfoUpdateRequest(); request.country = this.taxInfo.country; request.postalCode = this.taxInfo.postalCode; @@ -164,7 +187,7 @@ export class TaxInfoComponent { return this.organizationId ? this.organizationApiService.updateTaxInfo( this.organizationId, - request as OrganizationTaxInfoUpdateRequest, + request as ExpandedTaxInfoUpdateRequest, ) : this.apiService.putTaxInfo(request); } diff --git a/apps/web/src/app/components/selectable-avatar.component.ts b/apps/web/src/app/components/selectable-avatar.component.ts index 4a138ec989..1de722461a 100644 --- a/apps/web/src/app/components/selectable-avatar.component.ts +++ b/apps/web/src/app/components/selectable-avatar.component.ts @@ -41,13 +41,13 @@ export class SelectableAvatarComponent { .concat(["tw-cursor-pointer", "tw-outline", "tw-outline-solid", "tw-outline-offset-1"]) .concat( this.selected - ? ["tw-outline-[3px]", "tw-outline-primary-500"] + ? ["tw-outline-[3px]", "tw-outline-primary-600"] : [ "tw-outline-0", "hover:tw-outline-1", "hover:tw-outline-primary-300", "focus:tw-outline-2", - "focus:tw-outline-primary-500", + "focus:tw-outline-primary-600", ], ); } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index e2d3f64f2d..9d53bc39f0 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -1,6 +1,7 @@ import { CommonModule } from "@angular/common"; import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core"; +import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, STATE_FACTORY, @@ -12,12 +13,11 @@ import { OBSERVABLE_DISK_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE, WINDOW, + SafeInjectionToken, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -29,6 +29,7 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; +// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; @@ -59,102 +60,122 @@ import { Account, GlobalState, StateService } from "./state"; import { WebFileDownloadService } from "./web-file-download.service"; import { WebPlatformUtilsService } from "./web-platform-utils.service"; +/** + * Provider definitions used in the ngModule. + * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. + * If you need help please ask for it, do NOT change the type of this array. + */ +const safeProviders: SafeProvider[] = [ + safeProvider(InitService), + safeProvider(RouterService), + safeProvider(EventService), + safeProvider(PolicyListService), + safeProvider({ + provide: APP_INITIALIZER as SafeInjectionToken<() => void>, + useFactory: (initService: InitService) => initService.init(), + deps: [InitService], + multi: true, + }), + safeProvider({ + provide: STATE_FACTORY, + useValue: new StateFactory(GlobalState, Account), + }), + safeProvider({ + provide: STATE_SERVICE_USE_CACHE, + useValue: false, + }), + safeProvider({ + provide: I18nServiceAbstraction, + useClass: I18nService, + deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], + }), + safeProvider({ provide: AbstractStorageService, useClass: HtmlStorageService, deps: [] }), + safeProvider({ + provide: SECURE_STORAGE, + // TODO: platformUtilsService.isDev has a helper for this, but using that service here results in a circular dependency. + // We have a tech debt item in the backlog to break up platformUtilsService, but in the meantime simply checking the environment here is less cumbersome. + useClass: process.env.NODE_ENV === "development" ? HtmlStorageService : MemoryStorageService, + deps: [], + }), + safeProvider({ + provide: MEMORY_STORAGE, + useClass: MemoryStorageService, + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_MEMORY_STORAGE, + useClass: MemoryStorageServiceForStateProviders, + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_DISK_STORAGE, + useFactory: () => new WindowStorageService(window.sessionStorage), + deps: [], + }), + safeProvider({ + provide: PlatformUtilsServiceAbstraction, + useClass: WebPlatformUtilsService, + useAngularDecorators: true, + }), + safeProvider({ + provide: MessagingServiceAbstraction, + useClass: BroadcasterMessagingService, + useAngularDecorators: true, + }), + safeProvider({ + provide: ModalServiceAbstraction, + useClass: ModalService, + useAngularDecorators: true, + }), + safeProvider(StateService), + safeProvider({ + provide: BaseStateServiceAbstraction, + useExisting: StateService, + }), + safeProvider({ + provide: FileDownloadService, + useClass: WebFileDownloadService, + useAngularDecorators: true, + }), + safeProvider(CollectionAdminService), + safeProvider({ + provide: WindowStorageService, + useFactory: () => new WindowStorageService(window.localStorage), + deps: [], + }), + safeProvider({ + provide: OBSERVABLE_DISK_LOCAL_STORAGE, + useExisting: WindowStorageService, + }), + safeProvider({ + provide: StorageServiceProvider, + useClass: WebStorageServiceProvider, + deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE], + }), + safeProvider({ + provide: MigrationRunner, + useClass: WebMigrationRunner, + deps: [AbstractStorageService, LogService, MigrationBuilderService, WindowStorageService], + }), + safeProvider({ + provide: EnvironmentService, + useClass: WebEnvironmentService, + deps: [WINDOW, StateProvider, AccountService], + }), + safeProvider({ + provide: ThemeStateService, + useFactory: (globalStateProvider: GlobalStateProvider) => + // Web chooses to have Light as the default theme + new DefaultThemeStateService(globalStateProvider, ThemeType.Light), + deps: [GlobalStateProvider], + }), +]; + @NgModule({ declarations: [], imports: [CommonModule, JslibServicesModule], - providers: [ - InitService, - RouterService, - EventService, - PolicyListService, - { - provide: APP_INITIALIZER, - useFactory: (initService: InitService) => initService.init(), - deps: [InitService], - multi: true, - }, - { - provide: STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }, - { - provide: STATE_SERVICE_USE_CACHE, - useValue: false, - }, - { - provide: I18nServiceAbstraction, - useClass: I18nService, - deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], - }, - { provide: AbstractStorageService, useClass: HtmlStorageService }, - { - provide: SECURE_STORAGE, - // TODO: platformUtilsService.isDev has a helper for this, but using that service here results in a circular dependency. - // We have a tech debt item in the backlog to break up platformUtilsService, but in the meantime simply checking the environment here is less cumbersome. - useClass: process.env.NODE_ENV === "development" ? HtmlStorageService : MemoryStorageService, - }, - { - provide: MEMORY_STORAGE, - useClass: MemoryStorageService, - }, - { provide: OBSERVABLE_MEMORY_STORAGE, useClass: MemoryStorageServiceForStateProviders }, - { - provide: OBSERVABLE_DISK_STORAGE, - useFactory: () => new WindowStorageService(window.sessionStorage), - }, - { - provide: PlatformUtilsServiceAbstraction, - useClass: WebPlatformUtilsService, - }, - { provide: MessagingServiceAbstraction, useClass: BroadcasterMessagingService }, - { provide: ModalServiceAbstraction, useClass: ModalService }, - StateService, - { - provide: BaseStateServiceAbstraction, - useExisting: StateService, - }, - { - provide: FileDownloadService, - useClass: WebFileDownloadService, - }, - { - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateService], - }, - CollectionAdminService, - { - provide: OBSERVABLE_DISK_LOCAL_STORAGE, - useFactory: () => new WindowStorageService(window.localStorage), - }, - { - provide: StorageServiceProvider, - useClass: WebStorageServiceProvider, - deps: [OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_DISK_LOCAL_STORAGE], - }, - { - provide: MigrationRunner, - useClass: WebMigrationRunner, - deps: [ - AbstractStorageService, - LogService, - MigrationBuilderService, - OBSERVABLE_DISK_LOCAL_STORAGE, - ], - }, - { - provide: EnvironmentService, - useClass: WebEnvironmentService, - deps: [WINDOW, StateProvider, AccountService], - }, - { - provide: ThemeStateService, - useFactory: (globalStateProvider: GlobalStateProvider) => - // Web chooses to have Light as the default theme - new DefaultThemeStateService(globalStateProvider, ThemeType.Light), - deps: [GlobalStateProvider], - }, - ], + // Do not register your dependency here! Add it to the typesafeProviders array using the helper function + providers: safeProviders, }) export class CoreModule { constructor(@Optional() @SkipSelf() parentModule?: CoreModule) { diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index 60f3dea915..d5576d3bf7 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -10,7 +10,6 @@ import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/pla import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -28,7 +27,6 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, - private configService: ConfigService, @Inject(DOCUMENT) private document: Document, ) {} @@ -46,8 +44,6 @@ export class InitService { this.themingService.applyThemeChangesTo(this.document); const containerService = new ContainerService(this.cryptoService, this.encryptService); containerService.attachToGlobal(this.win); - - this.configService.init(); }; } } diff --git a/apps/web/src/app/core/router.service.ts b/apps/web/src/app/core/router.service.ts index 5a0d903ba7..caebb22733 100644 --- a/apps/web/src/app/core/router.service.ts +++ b/apps/web/src/app/core/router.service.ts @@ -1,14 +1,31 @@ import { Injectable } from "@angular/core"; import { Title } from "@angular/platform-browser"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; -import { filter } from "rxjs"; +import { filter, firstValueFrom } from "rxjs"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + KeyDefinition, + ROUTER_DISK, + StateProvider, + GlobalState, +} from "@bitwarden/common/platform/state"; + +const DEEP_LINK_REDIRECT_URL = new KeyDefinition(ROUTER_DISK, "deepLinkRedirectUrl", { + deserializer: (value: string) => value, +}); @Injectable() export class RouterService { + /** + * The string value of the URL the user tried to navigate to while unauthenticated. + * + * Developed to allow users to deep link even when the navigation gets interrupted + * by the authentication process. + */ + private deepLinkRedirectUrlState: GlobalState; + private previousUrl: string = undefined; private currentUrl: string = undefined; @@ -16,9 +33,11 @@ export class RouterService { private router: Router, private activatedRoute: ActivatedRoute, private titleService: Title, - private stateService: StateService, + private stateProvider: StateProvider, i18nService: I18nService, ) { + this.deepLinkRedirectUrlState = this.stateProvider.getGlobal(DEEP_LINK_REDIRECT_URL); + this.currentUrl = this.router.url; router.events @@ -67,14 +86,14 @@ export class RouterService { * @param url URL being saved to the Global State */ async persistLoginRedirectUrl(url: string): Promise { - await this.stateService.setDeepLinkRedirectUrl(url); + await this.deepLinkRedirectUrlState.update(() => url); } /** * Fetch and clear persisted LoginRedirectUrl if present in state */ async getAndClearLoginRedirectUrl(): Promise | undefined { - const persistedPreLoginUrl = await this.stateService.getDeepLinkRedirectUrl(); + const persistedPreLoginUrl = await firstValueFrom(this.deepLinkRedirectUrlState.state$); if (!Utils.isNullOrEmpty(persistedPreLoginUrl)) { await this.persistLoginRedirectUrl(null); diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index a80384d179..1ae62d8591 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -18,8 +18,6 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; -import { SendData } from "@bitwarden/common/tools/send/models/data/send.data"; -import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Account } from "./account"; import { GlobalState } from "./global-state"; @@ -58,32 +56,6 @@ export class StateService extends BaseStateService { await super.addAccount(account); } - async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.getEncryptedCiphers(options); - } - - async setEncryptedCiphers( - value: { [id: string]: CipherData }, - options?: StorageOptions, - ): Promise { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.setEncryptedCiphers(value, options); - } - - async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.getEncryptedSends(options); - } - - async setEncryptedSends( - value: { [id: string]: SendData }, - options?: StorageOptions, - ): Promise { - options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); - return await super.setEncryptedSends(value, options); - } - override async getLastSync(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); return await super.getLastSync(options); diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index 514e5deebd..e24013de6f 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -1,16 +1,29 @@ - {{ "newWebApp" | i18n }} + {{ unassignedItemsBannerService.bannerText$ | async | i18n }} + {{ "unassignedItemsBannerCTAPartOne" | i18n }} {{ "adminConsole" | i18n }} + {{ "unassignedItemsBannerCTAPartTwo" | i18n }} + {{ "releaseBlog" | i18n }}{{ "learnMore" | i18n }}
    ; protected selfHosted: boolean; protected hostname = location.hostname; + protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.UnassignedItemsBanner, + ); constructor( private route: ActivatedRoute, @@ -38,7 +42,8 @@ export class WebHeaderComponent { private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, - protected webLayoutMigrationBannerService: WebLayoutMigrationBannerService, + protected unassignedItemsBannerService: UnassignedItemsBannerService, + private configService: ConfigService, ) { this.routeData$ = this.route.data.pipe( map((params) => { diff --git a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts index 2c44a045c7..fc77e79a12 100644 --- a/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts +++ b/apps/web/src/app/layouts/org-switcher/org-switcher.component.ts @@ -46,7 +46,6 @@ export class OrgSwitcherComponent { /** * Visibility of the New Organization button - * (Temporary; will be removed when ability to create organizations is added to SM.) */ @Input() hideNewButton = false; diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index 9068f9c071..f038fafecc 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -14,10 +14,10 @@ [routerLink]="product.appRoute" [ngClass]=" product.isActive - ? 'tw-bg-primary-500 tw-font-bold !tw-text-contrast tw-ring-offset-2 hover:tw-bg-primary-500' + ? 'tw-bg-primary-600 tw-font-bold !tw-text-contrast tw-ring-offset-2 hover:tw-bg-primary-600' : '' " - class="tw-group tw-flex tw-h-24 tw-w-28 tw-flex-col tw-items-center tw-justify-center tw-rounded tw-p-1 tw-text-primary-500 tw-outline-none hover:tw-bg-background-alt hover:tw-text-primary-700 hover:tw-no-underline focus-visible:!tw-ring-2 focus-visible:!tw-ring-primary-700" + class="tw-group tw-flex tw-h-24 tw-w-28 tw-flex-col tw-items-center tw-justify-center tw-rounded tw-p-1 tw-text-primary-600 tw-outline-none hover:tw-bg-background-alt hover:tw-text-primary-700 hover:tw-no-underline focus-visible:!tw-ring-2 focus-visible:!tw-ring-primary-700" ariaCurrentWhenActive="page" > diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher.component.html index 449557b6f4..c9956f05e4 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.html @@ -1,10 +1,8 @@ - - - - + + diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts index a461785c31..eff5f08702 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher.component.ts @@ -1,16 +1,11 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input } from "@angular/core"; import { IconButtonType } from "@bitwarden/components/src/icon-button/icon-button.component"; - -import { flagEnabled } from "../../../utils/flags"; - @Component({ selector: "product-switcher", templateUrl: "./product-switcher.component.html", }) export class ProductSwitcherComponent implements AfterViewInit { - protected isEnabled = flagEnabled("secretsManager"); - /** * Passed to the product switcher's `bitIconButton` */ diff --git a/apps/web/src/app/layouts/toggle-width.component.ts b/apps/web/src/app/layouts/toggle-width.component.ts new file mode 100644 index 0000000000..0497416d62 --- /dev/null +++ b/apps/web/src/app/layouts/toggle-width.component.ts @@ -0,0 +1,33 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { NavigationModule } from "@bitwarden/components"; + +@Component({ + selector: "app-toggle-width", + template: ``, + standalone: true, + imports: [CommonModule, NavigationModule], +}) +export class ToggleWidthComponent { + protected isDev: boolean; + + constructor(platformUtilsService: PlatformUtilsService) { + this.isDev = platformUtilsService.isDev(); + } + + protected toggleWidth() { + if (document.body.style.minWidth === "unset") { + document.body.style.minWidth = ""; + } else { + document.body.style.minWidth = "unset"; + } + } +} diff --git a/apps/web/src/app/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html index 397e95d485..15a01fa07b 100644 --- a/apps/web/src/app/layouts/user-layout.component.html +++ b/apps/web/src/app/layouts/user-layout.component.html @@ -19,7 +19,7 @@ + + ; + protected showSubscription$: Observable; protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, @@ -45,8 +44,6 @@ export class UserLayoutComponent implements OnInit, OnDestroy { ); constructor( - private broadcasterService: BroadcasterService, - private ngZone: NgZone, private platformUtilsService: PlatformUtilsService, private organizationService: OrganizationService, private apiService: ApiService, @@ -58,43 +55,28 @@ export class UserLayoutComponent implements OnInit, OnDestroy { async ngOnInit() { document.body.classList.remove("layout_frontend"); - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "purchasedPremium": - await this.load(); - break; - default: - } - }); - }); - await this.syncService.fullSync(false); - await this.load(); - } - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } + this.hasFamilySponsorshipAvailable$ = this.organizationService.canManageSponsorships$; - async load() { - const hasPremiumPersonally = await firstValueFrom( + // We want to hide the subscription menu for organizations that provide premium. + // Except if the user has premium personally or has a billing history. + this.showSubscription$ = combineLatest([ this.billingAccountProfileStateService.hasPremiumPersonally$, - ); - const hasPremiumFromOrg = await firstValueFrom( this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$, - ); - const selfHosted = this.platformUtilsService.isSelfHost(); + ]).pipe( + concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => { + const isCloud = !this.platformUtilsService.isSelfHost(); - this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships(); - let billing = null; - if (!selfHosted) { - // TODO: We should remove the need to call this! - billing = await this.apiService.getUserBillingHistory(); - } - this.hideSubscription = - !hasPremiumPersonally && hasPremiumFromOrg && (selfHosted || billing?.hasNoHistory); + let billing = null; + if (isCloud) { + // TODO: We should remove the need to call this! + billing = await this.apiService.getUserBillingHistory(); + } + + const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory; + return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory; + }), + ); } } diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 9f4e59350e..93d68d31b3 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -14,6 +14,7 @@ import { flagEnabled, Flags } from "../utils/flags"; import { VerifyRecoverDeleteOrgComponent } from "./admin-console/organizations/manage/verify-recover-delete-org.component"; import { AcceptFamilySponsorshipComponent } from "./admin-console/organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "./admin-console/organizations/sponsorships/families-for-enterprise-setup.component"; +import { VerifyRecoverDeleteProviderComponent } from "./admin-console/providers/verify-recover-delete-provider.component"; import { CreateOrganizationComponent } from "./admin-console/settings/create-organization.component"; import { SponsoredFamiliesComponent } from "./admin-console/settings/sponsored-families.component"; import { AcceptOrganizationComponent } from "./auth/accept-organization.component"; @@ -163,6 +164,12 @@ const routes: Routes = [ canActivate: [UnauthGuard], data: { titleId: "deleteOrganization" }, }, + { + path: "verify-recover-delete-provider", + component: VerifyRecoverDeleteProviderComponent, + canActivate: [UnauthGuard], + data: { titleId: "deleteAccount" }, + }, { path: "send/:sendId/:key", component: AccessComponent, diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 73c03fd5dc..3f18440d23 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -1,6 +1,5 @@ import { NgModule } from "@angular/core"; -import { OrganizationUserModule } from "./admin-console/organizations/users/organization-user.module"; import { AuthModule } from "./auth"; import { LoginModule } from "./auth/login/login.module"; import { TrialInitiationModule } from "./auth/trial-initiation/trial-initiation.module"; @@ -16,7 +15,6 @@ import { VaultFilterModule } from "./vault/individual-vault/vault-filter/vault-f TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, - OrganizationUserModule, LoginModule, AuthModule, AccessComponent, diff --git a/apps/web/src/app/platform/web-migration-runner.ts b/apps/web/src/app/platform/web-migration-runner.ts index e8f44f7f41..4bd1d2d4b5 100644 --- a/apps/web/src/app/platform/web-migration-runner.ts +++ b/apps/web/src/app/platform/web-migration-runner.ts @@ -46,7 +46,7 @@ class WebMigrationHelper extends MigrationHelper { storageService: WindowStorageService, logService: LogService, ) { - super(currentVersion, storageService, logService); + super(currentVersion, storageService, logService, "web-disk-local"); this.diskLocalStorageService = storageService; } diff --git a/apps/web/src/app/settings/low-kdf.component.html b/apps/web/src/app/settings/low-kdf.component.html index e140f345e9..fd191b21e8 100644 --- a/apps/web/src/app/settings/low-kdf.component.html +++ b/apps/web/src/app/settings/low-kdf.component.html @@ -1,5 +1,5 @@ -
    -
    +
    +
    {{ "lowKdfIterations" | i18n }}
    diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts deleted file mode 100644 index b5b198d0ac..0000000000 --- a/apps/web/src/app/settings/settings.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; - -const BroadcasterSubscriptionId = "SettingsComponent"; - -@Component({ - selector: "app-settings", - templateUrl: "settings.component.html", -}) -export class SettingsComponent implements OnInit, OnDestroy { - premium: boolean; - selfHosted: boolean; - hasFamilySponsorshipAvailable: boolean; - hideSubscription: boolean; - - constructor( - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService, - private organizationService: OrganizationService, - private apiService: ApiService, - private billingAccountProfileStateServiceAbstraction: BillingAccountProfileStateService, - ) {} - - async ngOnInit() { - this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "purchasedPremium": - await this.load(); - break; - default: - } - }); - }); - - this.selfHosted = await this.platformUtilsService.isSelfHost(); - await this.load(); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - async load() { - this.premium = await firstValueFrom( - this.billingAccountProfileStateServiceAbstraction.hasPremiumPersonally$, - ); - this.hasFamilySponsorshipAvailable = await this.organizationService.canManageSponsorships(); - const hasPremiumFromOrg = await firstValueFrom( - this.billingAccountProfileStateServiceAbstraction.hasPremiumFromAnyOrganization$, - ); - let billing = null; - if (!this.selfHosted) { - billing = await this.apiService.getUserBillingHistory(); - } - this.hideSubscription = - !this.premium && hasPremiumFromOrg && (this.selfHosted || billing?.hasNoHistory); - } -} diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.component.html b/apps/web/src/app/shared/components/onboarding/onboarding.component.html index 6a0bacbf89..ecf1eb75dd 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.component.html +++ b/apps/web/src/app/shared/components/onboarding/onboarding.component.html @@ -1,7 +1,7 @@
    - +
    {{ title }}
    diff --git a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts index c1529dc81e..4088b7335c 100644 --- a/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts +++ b/apps/web/src/app/shared/components/onboarding/onboarding.stories.ts @@ -39,7 +39,7 @@ const Template: Story = (args) => ({ template: ` diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 73a068e1b4..7c0b8c448e 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -14,6 +14,7 @@ import { ReusedPasswordsReportComponent as OrgReusedPasswordsReportComponent } f import { UnsecuredWebsitesReportComponent as OrgUnsecuredWebsitesReportComponent } from "../admin-console/organizations/tools/unsecured-websites-report.component"; import { WeakPasswordsReportComponent as OrgWeakPasswordsReportComponent } from "../admin-console/organizations/tools/weak-passwords-report.component"; import { ProvidersComponent } from "../admin-console/providers/providers.component"; +import { VerifyRecoverDeleteProviderComponent } from "../admin-console/providers/verify-recover-delete-provider.component"; import { SponsoredFamiliesComponent } from "../admin-console/settings/sponsored-families.component"; import { SponsoringOrgRowComponent } from "../admin-console/settings/sponsoring-org-row.component"; import { AcceptOrganizationComponent } from "../auth/accept-organization.component"; @@ -186,6 +187,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, ], exports: [ @@ -263,6 +265,7 @@ import { SharedModule } from "./shared.module"; VerifyEmailComponent, VerifyEmailTokenComponent, VerifyRecoverDeleteComponent, + VerifyRecoverDeleteProviderComponent, LowKdfComponent, HeaderModule, DangerZoneComponent, diff --git a/apps/web/src/app/tools/send/icons/expired-send.icon.ts b/apps/web/src/app/tools/send/icons/expired-send.icon.ts index b39cdca797..3ce0856a26 100644 --- a/apps/web/src/app/tools/send/icons/expired-send.icon.ts +++ b/apps/web/src/app/tools/send/icons/expired-send.icon.ts @@ -2,10 +2,10 @@ import { svgIcon } from "@bitwarden/components"; export const ExpiredSend = svgIcon` - - - - - + + + + + `; diff --git a/apps/web/src/app/tools/send/icons/no-send.icon.ts b/apps/web/src/app/tools/send/icons/no-send.icon.ts index 7811a4723b..f5494a4b3c 100644 --- a/apps/web/src/app/tools/send/icons/no-send.icon.ts +++ b/apps/web/src/app/tools/send/icons/no-send.icon.ts @@ -2,12 +2,12 @@ import { svgIcon } from "@bitwarden/components"; export const NoSend = svgIcon` - - - - - - - + + + + + + + `; diff --git a/apps/web/src/app/tools/send/send.component.ts b/apps/web/src/app/tools/send/send.component.ts index b5edd1433e..755435882a 100644 --- a/apps/web/src/app/tools/send/send.component.ts +++ b/apps/web/src/app/tools/send/send.component.ts @@ -92,6 +92,7 @@ export class SendComponent extends BaseSendComponent { } ngOnDestroy() { + this.dialogService.closeAll(); this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); } diff --git a/apps/web/src/app/tools/vault-export/export.component.ts b/apps/web/src/app/tools/vault-export/export.component.ts index 8b14092f20..4fdd3ff9e0 100644 --- a/apps/web/src/app/tools/vault-export/export.component.ts +++ b/apps/web/src/app/tools/vault-export/export.component.ts @@ -1,7 +1,6 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; -import { ExportComponent as BaseExportComponent } from "@bitwarden/angular/tools/export/components/export.component"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -13,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; +import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui"; @Component({ selector: "app-export", @@ -95,7 +95,6 @@ export class ExportComponent extends BaseExportComponent { } const result = await UserVerificationDialogComponent.open(this.dialogService, { - clientSideOnlyVerification: true, title: "confirmVaultExport", bodyText: confirmDescription, confirmButtonOptions: { diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index d0ffb90911..b64ce5bb00 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -65,17 +65,22 @@
    - {{ "grantCollectionAccess" | i18n }} - {{ - "grantCollectionAccessMembersOnly" | i18n - }} - {{ " " + ("adminCollectionAccess" | i18n) }} + + {{ "readOnlyCollectionAccess" | i18n }} + + + {{ "grantCollectionAccess" | i18n }} + {{ + "grantCollectionAccessMembersOnly" | i18n + }} + {{ " " + ("adminCollectionAccess" | i18n) }} +
    - +
    -
    -
    +
    +
    + +
    @@ -313,7 +322,7 @@ > -
    - +
    + +
    diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts index 35e3a8bad3..cf9af4f68a 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/setup/setup.component.ts @@ -1,17 +1,20 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderSetupRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-setup.request"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction as ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ProviderKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { TaxInfoComponent } from "@bitwarden/web-vault/app/billing"; @Component({ selector: "provider-setup", @@ -19,6 +22,8 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class SetupComponent implements OnInit { + @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; + loading = true; authed = false; email: string; @@ -34,16 +39,21 @@ export class SetupComponent implements OnInit { false, ); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + false, + ); + constructor( private router: Router, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private route: ActivatedRoute, private cryptoService: CryptoService, - private apiService: ApiService, private syncService: SyncService, private validationService: ValidationService, private configService: ConfigService, + private providerApiService: ProviderApiServiceAbstraction, ) {} ngOnInit() { @@ -70,7 +80,7 @@ export class SetupComponent implements OnInit { // Check if provider exists, redirect if it does try { - const provider = await this.apiService.getProvider(this.providerId); + const provider = await this.providerApiService.getProvider(this.providerId); if (provider.name != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -102,7 +112,23 @@ export class SetupComponent implements OnInit { request.token = this.token; request.key = key; - const provider = await this.apiService.postProviderSetup(this.providerId, request); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + + if (enableConsolidatedBilling) { + request.taxInfo = new ExpandedTaxInfoUpdateRequest(); + const taxInfoView = this.taxInfoComponent.taxInfo; + request.taxInfo.country = taxInfoView.country; + request.taxInfo.postalCode = taxInfoView.postalCode; + if (taxInfoView.includeTaxId) { + request.taxInfo.taxId = taxInfoView.taxId; + request.taxInfo.line1 = taxInfoView.line1; + request.taxInfo.line2 = taxInfoView.line2; + request.taxInfo.city = taxInfoView.city; + request.taxInfo.state = taxInfoView.state; + } + } + + const provider = await this.providerApiService.postProviderSetup(this.providerId, request); this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup")); await this.syncService.fullSync(true); diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html index 2c02e89e24..2e4d8fcc44 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.html @@ -1,543 +1,549 @@ - - - {{ "loading" | i18n }} - + + + + {{ "loading" | i18n }} + -
    -

    - {{ "ssoPolicyHelpStart" | i18n }} - {{ "ssoPolicyHelpAnchor" | i18n }} - {{ "ssoPolicyHelpEnd" | i18n }} -
    -

    + +

    + {{ "ssoPolicyHelpStart" | i18n }} + {{ "ssoPolicyHelpAnchor" | i18n }} + {{ "ssoPolicyHelpEnd" | i18n }} +
    +

    - - - - {{ "allowSso" | i18n }} - - {{ "allowSsoDesc" | i18n }} - - - - {{ "ssoIdentifier" | i18n }} - - - {{ "ssoIdentifierHintPartOne" | i18n }} - {{ "domainVerification" | i18n }} - - - -
    - - - {{ "memberDecryptionOption" | i18n }} - - - {{ "masterPass" | i18n }} - - - - - {{ "keyConnector" | i18n }} - - - - - - {{ "memberDecryptionKeyConnectorDescStart" | i18n }} - {{ "memberDecryptionKeyConnectorDescLink" | i18n }} - {{ "memberDecryptionKeyConnectorDescEnd" | i18n }} - - - - - - {{ "trustedDevices" | i18n }} - - - {{ "memberDecryptionOptionTdeDescriptionPartOne" | i18n }} - {{ "memberDecryptionOptionTdeDescriptionLinkOne" | i18n }} - {{ "memberDecryptionOptionTdeDescriptionPartTwo" | i18n }} - {{ "memberDecryptionOptionTdeDescriptionLinkTwo" | i18n }} - {{ "memberDecryptionOptionTdeDescriptionPartThree" | i18n }} - {{ - "memberDecryptionOptionTdeDescriptionLinkThree" | i18n - }} - {{ "memberDecryptionOptionTdeDescriptionPartFour" | i18n }} - - - - - - - - {{ "keyConnectorWarning" | i18n }} - + + + + {{ "allowSso" | i18n }} + + {{ "allowSsoDesc" | i18n }} + - {{ "keyConnectorUrl" | i18n }} - - - - - - {{ "keyConnectorTestSuccess" | i18n }} - + {{ "ssoIdentifier" | i18n }} + + + {{ "ssoIdentifierHintPartOne" | i18n }} + {{ "domainVerification" | i18n }} + +
    + + + {{ "memberDecryptionOption" | i18n }} + + + {{ "masterPass" | i18n }} + + + + + {{ "keyConnector" | i18n }} + + + + + + {{ "memberDecryptionKeyConnectorDescStart" | i18n }} + {{ "memberDecryptionKeyConnectorDescLink" | i18n }} + {{ "memberDecryptionKeyConnectorDescEnd" | i18n }} + + + + + + {{ "trustedDevices" | i18n }} + + + {{ "memberDecryptionOptionTdeDescriptionPartOne" | i18n }} + {{ + "memberDecryptionOptionTdeDescriptionLinkOne" | i18n + }} + {{ "memberDecryptionOptionTdeDescriptionPartTwo" | i18n }} + {{ + "memberDecryptionOptionTdeDescriptionLinkTwo" | i18n + }} + {{ "memberDecryptionOptionTdeDescriptionPartThree" | i18n }} + {{ + "memberDecryptionOptionTdeDescriptionLinkThree" | i18n + }} + {{ "memberDecryptionOptionTdeDescriptionPartFour" | i18n }} + + + + + + + + {{ "keyConnectorWarning" | i18n }} + + + + {{ "keyConnectorUrl" | i18n }} + + + + + + {{ "keyConnectorTestSuccess" | i18n }} + + + + + +
    + + + {{ "type" | i18n }} + +
    -
    + +
    +
    +

    {{ "openIdConnectConfig" | i18n }}

    - - {{ "type" | i18n }} - - - + + {{ "callbackPath" | i18n }} + + + - -
    -
    -

    {{ "openIdConnectConfig" | i18n }}

    + + {{ "signedOutCallbackPath" | i18n }} + + + - - {{ "callbackPath" | i18n }} - - - + + {{ "authority" | i18n }} + + - - {{ "signedOutCallbackPath" | i18n }} - - - + + {{ "clientId" | i18n }} + + - - {{ "authority" | i18n }} - - + + {{ "clientSecret" | i18n }} + + - - {{ "clientId" | i18n }} - - + + {{ "metadataAddress" | i18n }} + + {{ "openIdAuthorityRequired" | i18n }} + - - {{ "clientSecret" | i18n }} - - + + {{ "oidcRedirectBehavior" | i18n }} + + - - {{ "metadataAddress" | i18n }} - - {{ "openIdAuthorityRequired" | i18n }} - + + {{ "getClaimsFromUserInfoEndpoint" | i18n }} + + - - {{ "oidcRedirectBehavior" | i18n }} - - - - - {{ "getClaimsFromUserInfoEndpoint" | i18n }} - - - - -
    -

    - {{ "openIdOptionalCustomizations" | i18n }} -

    - -
    -
    - - {{ "additionalScopes" | i18n }} - - {{ "separateMultipleWithComma" | i18n }} - +

    + {{ "openIdOptionalCustomizations" | i18n }} +

    + +
    +
    + + {{ "additionalScopes" | i18n }} + + {{ "separateMultipleWithComma" | i18n }} + - - {{ "additionalUserIdClaimTypes" | i18n }} - - {{ "separateMultipleWithComma" | i18n }} - + + {{ "additionalUserIdClaimTypes" | i18n }} + + {{ "separateMultipleWithComma" | i18n }} + - - {{ "additionalEmailClaimTypes" | i18n }} - - {{ "separateMultipleWithComma" | i18n }} - + + {{ "additionalEmailClaimTypes" | i18n }} + + {{ "separateMultipleWithComma" | i18n }} + - - {{ "additionalNameClaimTypes" | i18n }} - - {{ "separateMultipleWithComma" | i18n }} - + + {{ "additionalNameClaimTypes" | i18n }} + + {{ "separateMultipleWithComma" | i18n }} + - - {{ "acrValues" | i18n }} - - acr_values - + + {{ "acrValues" | i18n }} + + acr_values + - - {{ "expectedReturnAcrValue" | i18n }} - - acr_validaton - + + {{ "expectedReturnAcrValue" | i18n }} + + acr_validaton + +
    -
    - -
    -
    -

    {{ "samlSpConfig" | i18n }}

    +
    + +
    +

    {{ "samlSpConfig" | i18n }}

    - - {{ "spUniqueEntityId" | i18n }} - - {{ "spUniqueEntityIdDesc" | i18n }} - + + {{ "spUniqueEntityId" | i18n }} + + {{ "spUniqueEntityIdDesc" | i18n }} + - - {{ "spEntityId" | i18n }} - - - + + {{ "spEntityId" | i18n }} + + + - - {{ "spEntityId" | i18n }} - - - + + {{ "spEntityId" | i18n }} + + + - - {{ "spMetadataUrl" | i18n }} - - - - - - - {{ "spAcsUrl" | i18n }} - - - - - - {{ "spNameIdFormat" | i18n }} - + + + - - {{ "spOutboundSigningAlgorithm" | i18n }} - - + + {{ "spAcsUrl" | i18n }} + + + - - {{ "spSigningBehavior" | i18n }} - - + + {{ "spNameIdFormat" | i18n }} + + - - {{ "spMinIncomingSigningAlgorithm" | i18n }} - - + + {{ "spOutboundSigningAlgorithm" | i18n }} + + - - {{ "spWantAssertionsSigned" | i18n }} - - + + {{ "spSigningBehavior" | i18n }} + + - - {{ "spValidateCertificates" | i18n }} - - + + {{ "spMinIncomingSigningAlgorithm" | i18n }} + + + + + {{ "spWantAssertionsSigned" | i18n }} + + + + + {{ "spValidateCertificates" | i18n }} + + +
    + + +
    +

    {{ "samlIdpConfig" | i18n }}

    + + + {{ "idpEntityId" | i18n }} + + + + + {{ "idpBindingType" | i18n }} + + + + + {{ "idpSingleSignOnServiceUrl" | i18n }} + + {{ "idpSingleSignOnServiceUrlRequired" | i18n }} + + + + {{ "idpSingleLogoutServiceUrl" | i18n }} + + + + + {{ "idpX509PublicCert" | i18n }} + + + + + {{ "idpOutboundSigningAlgorithm" | i18n }} + + + + + + + + {{ "idpAllowOutboundLogoutRequests" | i18n }} + + + + + {{ "idpSignAuthenticationRequests" | i18n }} + + +
    - -
    -

    {{ "samlIdpConfig" | i18n }}

    - - - {{ "idpEntityId" | i18n }} - - - - - {{ "idpBindingType" | i18n }} - - - - - {{ "idpSingleSignOnServiceUrl" | i18n }} - - {{ "idpSingleSignOnServiceUrlRequired" | i18n }} - - - - {{ "idpSingleLogoutServiceUrl" | i18n }} - - - - - {{ "idpX509PublicCert" | i18n }} - - - - - {{ "idpOutboundSigningAlgorithm" | i18n }} - - - - - - - - {{ "idpAllowOutboundLogoutRequests" | i18n }} - - - - - {{ "idpSignAuthenticationRequests" | i18n }} - - -
    -
    - - - - + + + + diff --git a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts index d5a1aebdd8..45cfc02a09 100644 --- a/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/auth/sso/sso.component.ts @@ -26,7 +26,7 @@ import { SsoConfigApi } from "@bitwarden/common/auth/models/api/sso-config.api"; import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/organization-sso.request"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -186,7 +186,7 @@ export class SsoComponent implements OnInit, OnDestroy { private i18nService: I18nService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit() { diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.html new file mode 100644 index 0000000000..18d6b3e63c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.html @@ -0,0 +1,49 @@ + + + {{ "manageSeats" | i18n }} + {{ clientName }} + +
    +

    + {{ "manageSeatsDescription" | i18n }} +

    + + + {{ "assignedSeats" | i18n }} + + + + +

    + {{ unassignedSeats }} + {{ "unassignedSeatsDescription" | i18n }} +

    +

    + {{ AdditionalSeatPurchased }} + {{ "purchaseSeatDescription" | i18n }} +

    +
    +
    + + + + +
    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts new file mode 100644 index 0000000000..2c8d59edc3 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organization-subscription.component.ts @@ -0,0 +1,115 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; + +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; +import { ProviderSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/provider-subscription-update.request"; +import { Plans } from "@bitwarden/common/billing/models/response/provider-subscription-response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService } from "@bitwarden/components"; + +type ManageClientOrganizationDialogParams = { + organization: ProviderOrganizationOrganizationDetailsResponse; +}; + +@Component({ + templateUrl: "manage-client-organization-subscription.component.html", +}) +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class ManageClientOrganizationSubscriptionComponent implements OnInit { + loading = true; + providerOrganizationId: string; + providerId: string; + + clientName: string; + assignedSeats: number; + unassignedSeats: number; + planName: string; + AdditionalSeatPurchased: number; + remainingOpenSeats: number; + + constructor( + public dialogRef: DialogRef, + @Inject(DIALOG_DATA) protected data: ManageClientOrganizationDialogParams, + private billingApiService: BillingApiService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + ) { + this.providerOrganizationId = data.organization.id; + this.providerId = data.organization.providerId; + this.clientName = data.organization.organizationName; + this.assignedSeats = data.organization.seats; + this.planName = data.organization.plan; + } + + async ngOnInit() { + try { + const response = await this.billingApiService.getProviderClientSubscriptions(this.providerId); + this.AdditionalSeatPurchased = this.getPurchasedSeatsByPlan(this.planName, response.plans); + const seatMinimum = this.getProviderSeatMinimumByPlan(this.planName, response.plans); + const assignedByPlan = this.getAssignedByPlan(this.planName, response.plans); + this.remainingOpenSeats = seatMinimum - assignedByPlan; + this.unassignedSeats = Math.abs(this.remainingOpenSeats); + } catch (error) { + this.remainingOpenSeats = 0; + this.AdditionalSeatPurchased = 0; + } + this.loading = false; + } + + async updateSubscription(assignedSeats: number) { + this.loading = true; + if (!assignedSeats) { + this.platformUtilsService.showToast( + "error", + null, + this.i18nService.t("assignedSeatCannotUpdate"), + ); + return; + } + + const request = new ProviderSubscriptionUpdateRequest(); + request.assignedSeats = assignedSeats; + + await this.billingApiService.putProviderClientSubscriptions( + this.providerId, + this.providerOrganizationId, + request, + ); + this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated")); + this.loading = false; + this.dialogRef.close(); + } + + getPurchasedSeatsByPlan(planName: string, plans: Plans[]): number { + const plan = plans.find((plan) => plan.planName === planName); + if (plan) { + return plan.purchasedSeats; + } else { + return 0; + } + } + + getAssignedByPlan(planName: string, plans: Plans[]): number { + const plan = plans.find((plan) => plan.planName === planName); + if (plan) { + return plan.assignedSeats; + } else { + return 0; + } + } + + getProviderSeatMinimumByPlan(planName: string, plans: Plans[]) { + const plan = plans.find((plan) => plan.planName === planName); + if (plan) { + return plan.seatMinimum; + } else { + return 0; + } + } + + static open(dialogService: DialogService, data: ManageClientOrganizationDialogParams) { + return dialogService.open(ManageClientOrganizationSubscriptionComponent, { data }); + } +} diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html new file mode 100644 index 0000000000..dc303d338f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.html @@ -0,0 +1,90 @@ + + + + + {{ "addNewOrganization" | i18n }} + + + + + + {{ "loading" | i18n }} + + + +

    {{ "noClientsInList" | i18n }}

    + + + + + {{ "client" | i18n }} + {{ "assigned" | i18n }} + {{ "used" | i18n }} + {{ "remaining" | i18n }} + {{ "billingPlan" | i18n }} + + + + + + + + + + + + + {{ client.seats }} + + + {{ client.userCount }} + + + {{ client.seats - client.userCount }} + + + {{ client.plan }} + + + + + + + + + + + + +
    diff --git a/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts new file mode 100644 index 0000000000..a9f341be94 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/billing/providers/clients/manage-client-organizations.component.ts @@ -0,0 +1,180 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; +import { first, switchMap, takeUntil } from "rxjs/operators"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { DialogService, TableDataSource } from "@bitwarden/components"; + +import { WebProviderService } from "../../../admin-console/providers/services/web-provider.service"; + +import { ManageClientOrganizationSubscriptionComponent } from "./manage-client-organization-subscription.component"; + +@Component({ + templateUrl: "manage-client-organizations.component.html", +}) + +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class ManageClientOrganizationsComponent implements OnInit, OnDestroy { + providerId: string; + loading = true; + manageOrganizations = false; + + private destroy$ = new Subject(); + private _searchText$ = new BehaviorSubject(""); + private isSearching: boolean = false; + + get searchText() { + return this._searchText$.value; + } + + set searchText(search: string) { + this._searchText$.value; + + this.selection.clear(); + this.dataSource.filter = search; + } + + clients: ProviderOrganizationOrganizationDetailsResponse[]; + pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; + + protected didScroll = false; + protected pageSize = 100; + protected actionPromise: Promise; + private pagedClientsCount = 0; + selection = new SelectionModel(true, []); + protected dataSource = new TableDataSource(); + + constructor( + private route: ActivatedRoute, + private providerService: ProviderService, + private apiService: ApiService, + private searchService: SearchService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + private dialogService: DialogService, + ) {} + + async ngOnInit() { + // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe + this.route.parent.params.subscribe(async (params) => { + this.providerId = params.providerId; + + await this.load(); + + /* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */ + this.route.queryParams.pipe(first()).subscribe(async (qParams) => { + this.searchText = qParams.search; + }); + }); + + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearching = isSearchable; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + async load() { + const response = await this.apiService.getProviderClients(this.providerId); + this.clients = response.data != null && response.data.length > 0 ? response.data : []; + this.dataSource.data = this.clients; + this.manageOrganizations = + (await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; + + this.loading = false; + } + + isPaging() { + const searching = this.isSearching; + if (searching && this.didScroll) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.resetPaging(); + } + return !searching && this.clients && this.clients.length > this.pageSize; + } + + async resetPaging() { + this.pagedClients = []; + this.loadMore(); + } + + loadMore() { + if (!this.clients || this.clients.length <= this.pageSize) { + return; + } + const pagedLength = this.pagedClients.length; + let pagedSize = this.pageSize; + if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { + pagedSize = this.pagedClientsCount; + } + if (this.clients.length > pagedLength) { + this.pagedClients = this.pagedClients.concat( + this.clients.slice(pagedLength, pagedLength + pagedSize), + ); + } + this.pagedClientsCount = this.pagedClients.length; + this.didScroll = this.pagedClients.length > this.pageSize; + } + + async manageSubscription(organization: ProviderOrganizationOrganizationDetailsResponse) { + if (organization == null) { + return; + } + + const dialogRef = ManageClientOrganizationSubscriptionComponent.open(this.dialogService, { + organization: organization, + }); + + await firstValueFrom(dialogRef.closed); + await this.load(); + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + this.actionPromise = this.webProviderService.detachOrganization( + this.providerId, + organization.id, + ); + try { + await this.actionPromise; + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("detachedOrganization", organization.organizationName), + ); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + this.actionPromise = null; + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.module.ts index ee50efb3e4..19d8f53a34 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/layout.module.ts @@ -2,13 +2,20 @@ import { NgModule } from "@angular/core"; import { LayoutComponent as BitLayoutComponent, NavigationModule } from "@bitwarden/components"; import { OrgSwitcherComponent } from "@bitwarden/web-vault/app/layouts/org-switcher/org-switcher.component"; +import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-width.component"; import { SharedModule } from "@bitwarden/web-vault/app/shared/shared.module"; import { LayoutComponent } from "./layout.component"; import { NavigationComponent } from "./navigation.component"; @NgModule({ - imports: [SharedModule, NavigationModule, BitLayoutComponent, OrgSwitcherComponent], + imports: [ + SharedModule, + NavigationModule, + BitLayoutComponent, + OrgSwitcherComponent, + ToggleWidthComponent, + ], declarations: [LayoutComponent, NavigationComponent], }) export class LayoutModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html index 51a163377d..e71f520996 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.html @@ -18,7 +18,7 @@ > @@ -41,4 +41,6 @@ [relativeTo]="route.parent" > + + diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts index cd117819a9..30f4308a39 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/layout/navigation.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { map } from "rxjs"; +import { concatMap } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -14,7 +14,9 @@ export class NavigationComponent { protected readonly logo = SecretsManagerLogo; protected orgFilter = (org: Organization) => org.canAccessSecretsManager; protected isAdmin$ = this.route.params.pipe( - map(async (params) => (await this.organizationService.get(params.organizationId))?.isAdmin), + concatMap( + async (params) => (await this.organizationService.get(params.organizationId))?.isAdmin, + ), ); constructor( diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html index 04d705af23..255877e4e8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html @@ -5,7 +5,7 @@
    >; -const SM_ONBOARDING_TASKS_KEY = new KeyDefinition(SM_ONBOARDING_DISK, "tasks", { - deserializer: (b) => b, -}); +const SM_ONBOARDING_TASKS_KEY = new UserKeyDefinition( + SM_ONBOARDING_DISK, + "tasks", + { + deserializer: (b) => b, + clearOn: [], // Used to track tasks completed by a user, we don't want to reshow if they've locked or logged out and came back to the app + }, +); @Injectable({ providedIn: "root", diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html index 2755377d2a..443711fd36 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.html @@ -1,14 +1,14 @@

    - {{ "projectServiceAccountsDescription" | i18n }} + {{ "projectMachineAccountsDescription" | i18n }}

    {{ "secrets" | i18n }} {{ "people" | i18n }} - {{ "serviceAccounts" | i18n }} + {{ "machineAccounts" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html index 9af3483703..5ef2be8ade 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.html @@ -8,7 +8,7 @@ {{ data.serviceAccounts.length }} - {{ "serviceAccounts" | i18n }} + {{ "machineAccounts" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts index 3d136aa92a..b31ef03d12 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-delete-dialog.component.ts @@ -43,14 +43,14 @@ export class ServiceAccountDeleteDialogComponent { get title() { return this.data.serviceAccounts.length === 1 - ? this.i18nService.t("deleteServiceAccount") - : this.i18nService.t("deleteServiceAccounts"); + ? this.i18nService.t("deleteMachineAccount") + : this.i18nService.t("deleteMachineAccounts"); } get dialogContent() { return this.data.serviceAccounts.length === 1 - ? this.i18nService.t("deleteServiceAccountDialogMessage", this.data.serviceAccounts[0].name) - : this.i18nService.t("deleteServiceAccountsDialogMessage"); + ? this.i18nService.t("deleteMachineAccountDialogMessage", this.data.serviceAccounts[0].name) + : this.i18nService.t("deleteMachineAccountsDialogMessage"); } get dialogConfirmationLabel() { @@ -79,17 +79,17 @@ export class ServiceAccountDeleteDialogComponent { const message = this.data.serviceAccounts.length === 1 - ? "deleteServiceAccountToast" - : "deleteServiceAccountsToast"; + ? "deleteMachineAccountToast" + : "deleteMachineAccountsToast"; this.platformUtilsService.showToast("success", null, this.i18nService.t(message)); } openBulkStatusDialog(bulkStatusResults: BulkOperationStatus[]) { this.dialogService.open(BulkStatusDialogComponent, { data: { - title: "deleteServiceAccounts", - subTitle: "serviceAccounts", - columnTitle: "serviceAccountName", + title: "deleteMachineAccounts", + subTitle: "machineAccounts", + columnTitle: "machineAccountName", message: "bulkDeleteProjectsErrorMessage", details: bulkStatusResults, }, @@ -100,7 +100,7 @@ export class ServiceAccountDeleteDialogComponent { return this.data.serviceAccounts?.length === 1 ? this.i18nService.t("deleteProjectConfirmMessage", this.data.serviceAccounts[0].name) : this.i18nService.t( - "deleteServiceAccountsConfirmMessage", + "deleteMachineAccountsConfirmMessage", this.data.serviceAccounts?.length.toString(), ); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html index 55f6ff4da1..0064341537 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.html @@ -7,7 +7,7 @@
    - {{ "serviceAccountName" | i18n }} + {{ "machineAccountName" | i18n }}
    diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts index 9aa7c658f3..105ca59e57 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/dialog/service-account-dialog.component.ts @@ -69,7 +69,7 @@ export class ServiceAccountDialogComponent { this.platformUtilsService.showToast( "error", null, - this.i18nService.t("serviceAccountsCannotCreate"), + this.i18nService.t("machineAccountsCannotCreate"), ); return; } @@ -85,14 +85,14 @@ export class ServiceAccountDialogComponent { if (this.data.operation == OperationType.Add) { await this.serviceAccountService.create(this.data.organizationId, serviceAccountView); - serviceAccountMessage = this.i18nService.t("serviceAccountCreated"); + serviceAccountMessage = this.i18nService.t("machineAccountCreated"); } else { await this.serviceAccountService.update( this.data.serviceAccountId, this.data.organizationId, serviceAccountView, ); - serviceAccountMessage = this.i18nService.t("serviceAccountUpdated"); + serviceAccountMessage = this.i18nService.t("machineAccountUpdated"); } this.platformUtilsService.showToast("success", null, serviceAccountMessage); @@ -107,6 +107,6 @@ export class ServiceAccountDialogComponent { } get title() { - return this.data.operation === OperationType.Add ? "newServiceAccount" : "editServiceAccount"; + return this.data.operation === OperationType.Add ? "newMachineAccount" : "editMachineAccount"; } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts index 1ef71811a1..554e7fa37d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/event-logs/service-accounts-events.component.ts @@ -65,7 +65,7 @@ export class ServiceAccountEventsComponent extends BaseEventsComponent implement protected getUserName() { return { - name: this.i18nService.t("serviceAccount") + " " + this.serviceAccountId, + name: this.i18nService.t("machineAccount") + " " + this.serviceAccountId, email: "", }; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html index 79c8132bbc..074fa8ca00 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html @@ -1,7 +1,7 @@

    - {{ "serviceAccountPeopleDescription" | i18n }} + {{ "machineAccountPeopleDescription" | i18n }}

    { const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "smAccessRemovalWarningSaTitle" }, - content: { key: "smAccessRemovalWarningSaMessage" }, + title: { key: "smAccessRemovalWarningMaTitle" }, + content: { key: "smAccessRemovalWarningMaMessage" }, acceptButtonText: { key: "removeAccess" }, cancelButtonText: { key: "cancel" }, type: "warning", @@ -222,7 +222,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { private async showAccessTokenStillAvailableWarning(): Promise { await this.dialogService.openSimpleDialog({ title: { key: "saPeopleWarningTitle" }, - content: { key: "saPeopleWarningMessage" }, + content: { key: "maPeopleWarningMessage" }, type: "warning", acceptButtonText: { key: "close" }, cancelButtonText: null, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html index 368a62a933..b97c5ef114 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.html @@ -1,6 +1,6 @@

    - {{ "serviceAccountProjectsDescription" | i18n }} + {{ "machineAccountProjectsDescription" | i18n }}

    {{ - "serviceAccounts" | i18n + "machineAccounts" | i18n }} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts index d352e8a246..083ec7aebb 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-account.component.ts @@ -49,7 +49,7 @@ export class ServiceAccountComponent implements OnInit, OnDestroy { this.platformUtilsService.showToast( "error", null, - this.i18nService.t("notFound", this.i18nService.t("serviceAccount")), + this.i18nService.t("notFound", this.i18nService.t("machineAccount")), ); }); return EMPTY; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html index fb8d953e10..bfb7b98542 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts-list.component.html @@ -3,8 +3,8 @@
    - {{ "serviceAccountsNoItemsTitle" | i18n }} - {{ "serviceAccountsNoItemsMessage" | i18n }} + {{ "machineAccountsNoItemsTitle" | i18n }} + {{ "machineAccountsNoItemsMessage" | i18n }} @@ -80,16 +80,16 @@ - {{ "viewServiceAccount" | i18n }} + {{ "viewMachineAccount" | i18n }} @@ -101,7 +101,7 @@ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html index 92ebcdbaac..d7a4f2c747 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.html @@ -1,6 +1,6 @@ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html index 528514e678..457eff37fa 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/new-menu.component.html @@ -19,6 +19,6 @@ diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts index 0cad3129a4..55dc2f8b71 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts @@ -2,7 +2,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { AuthGuard } from "@bitwarden/angular/auth/guards"; -import { buildFlaggedRoute } from "@bitwarden/web-vault/app/oss-routing.module"; import { organizationEnabledGuard } from "./guards/sm-org-enabled.guard"; import { canActivateSM } from "./guards/sm.guard"; @@ -17,7 +16,7 @@ import { OrgSuspendedComponent } from "./shared/org-suspended.component"; import { TrashModule } from "./trash/trash.module"; const routes: Routes = [ - buildFlaggedRoute("secretsManager", { + { path: "", children: [ { @@ -58,7 +57,7 @@ const routes: Routes = [ path: "service-accounts", loadChildren: () => ServiceAccountsModule, data: { - titleId: "serviceAccounts", + titleId: "machineAccounts", }, }, { @@ -86,7 +85,7 @@ const routes: Routes = [ ], }, ], - }), + }, ]; @NgModule({ diff --git a/libs/angular/jest.config.js b/libs/angular/jest.config.js index e294e4ff47..c8e748575c 100644 --- a/libs/angular/jest.config.js +++ b/libs/angular/jest.config.js @@ -10,7 +10,11 @@ module.exports = { displayName: "libs/angular tests", preset: "jest-preset-angular", setupFilesAfterEnv: ["/test.setup.ts"], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/", - }), + moduleNameMapper: pathsToModuleNameMapper( + // lets us use @bitwarden/common/spec in tests + { "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "/", + }, + ), }; diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 167fe0a97f..5f8c4145cb 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -59,7 +59,7 @@ export class CollectionsComponent implements OnInit { } } - async submit() { + async submit(): Promise { const selectedCollectionIds = this.collections .filter((c) => { if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { @@ -75,7 +75,7 @@ export class CollectionsComponent implements OnInit { this.i18nService.t("errorOccurred"), this.i18nService.t("selectOneCollection"), ); - return; + return false; } this.cipherDomain.collectionIds = selectedCollectionIds; try { @@ -83,8 +83,10 @@ export class CollectionsComponent implements OnInit { await this.formPromise; this.onSavedCollections.emit(); this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem")); + return true; } catch (e) { this.logService.error(e); + return false; } } diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 79202054c5..8345bb9939 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -15,15 +15,16 @@ import { } from "rxjs"; import { + LoginEmailServiceAbstraction, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -34,6 +35,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { UserId } from "@bitwarden/common/types/guid"; enum State { NewUser, @@ -65,6 +67,8 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected data?: Data; protected loading = true; + activeAccountId: UserId; + // Remember device means for the user to trust the device rememberDeviceForm = this.formBuilder.group({ rememberDevice: [true], @@ -82,7 +86,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected activatedRoute: ActivatedRoute, protected messagingService: MessagingService, protected tokenService: TokenService, - protected loginService: LoginService, + protected loginEmailService: LoginEmailServiceAbstraction, protected organizationApiService: OrganizationApiServiceAbstraction, protected cryptoService: CryptoService, protected organizationUserService: OrganizationUserService, @@ -94,10 +98,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, + protected accountService: AccountService, ) {} async ngOnInit() { this.loading = true; + this.activeAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id; this.setupRememberDeviceValueChanges(); @@ -150,7 +156,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { } private async setRememberDeviceDefaultValue() { - const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice(); + const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice( + this.activeAccountId, + ); const rememberDevice = rememberDeviceFromState ?? true; @@ -161,7 +169,9 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { this.rememberDevice.valueChanges .pipe( switchMap((value) => - defer(() => this.deviceTrustCryptoService.setShouldTrustDevice(value)), + defer(() => + this.deviceTrustCryptoService.setShouldTrustDevice(this.activeAccountId, value), + ), ), takeUntil(this.destroy$), ) @@ -244,23 +254,17 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { return; } - this.loginService.setEmail(this.data.userEmail); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/login-with-device"]); + this.loginEmailService.setEmail(this.data.userEmail); + await this.router.navigate(["/login-with-device"]); } async requestAdminApproval() { - this.loginService.setEmail(this.data.userEmail); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/admin-approval-requested"]); + this.loginEmailService.setEmail(this.data.userEmail); + await this.router.navigate(["/admin-approval-requested"]); } async approveWithMasterPassword() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } }); + await this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } }); } async createUser() { @@ -284,7 +288,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { await this.passwordResetEnrollmentService.enroll(this.data.organizationId); if (this.rememberDeviceForm.value.rememberDevice) { - await this.deviceTrustCryptoService.trustDevice(); + await this.deviceTrustCryptoService.trustDevice(this.activeAccountId); } } catch (error) { this.validationService.showError(error); diff --git a/libs/angular/src/auth/components/environment-selector.component.html b/libs/angular/src/auth/components/environment-selector.component.html index 8ad0602385..a8dab8f121 100644 --- a/libs/angular/src/auth/components/environment-selector.component.html +++ b/libs/angular/src/auth/components/environment-selector.component.html @@ -1,79 +1,81 @@ -
    - {{ "loggingInOn" | i18n }}: - -
    - - -
    - + + +
    +
    -
    -
    + + diff --git a/libs/angular/src/auth/components/environment-selector.component.ts b/libs/angular/src/auth/components/environment-selector.component.ts index 838667e080..9e811d02af 100644 --- a/libs/angular/src/auth/components/environment-selector.component.ts +++ b/libs/angular/src/auth/components/environment-selector.component.ts @@ -36,11 +36,9 @@ import { }) export class EnvironmentSelectorComponent { @Output() onOpenSelfHostedSettings = new EventEmitter(); - isOpen = false; - showingModal = false; - selectedEnvironment: Region; - ServerEnvironmentType = Region; - overlayPosition: ConnectedPosition[] = [ + protected isOpen = false; + protected ServerEnvironmentType = Region; + protected overlayPosition: ConnectedPosition[] = [ { originX: "start", originY: "bottom", diff --git a/libs/angular/src/auth/components/hint.component.ts b/libs/angular/src/auth/components/hint.component.ts index 54edc5b8fa..484604b6a5 100644 --- a/libs/angular/src/auth/components/hint.component.ts +++ b/libs/angular/src/auth/components/hint.component.ts @@ -1,8 +1,8 @@ import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; +import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -22,11 +22,11 @@ export class HintComponent implements OnInit { protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, private logService: LogService, - private loginService: LoginService, + private loginEmailService: LoginEmailServiceAbstraction, ) {} ngOnInit(): void { - this.email = this.loginService.getEmail() ?? ""; + this.email = this.loginEmailService.getEmail() ?? ""; } async submit() { diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index c21ba1a75a..6602a917c9 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -10,7 +10,9 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; @@ -55,6 +57,7 @@ export class LockComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected router: Router, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, @@ -75,6 +78,7 @@ export class LockComponent implements OnInit, OnDestroy { protected userVerificationService: UserVerificationService, protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, + protected accountService: AccountService, ) {} async ngOnInit() { @@ -204,6 +208,7 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const kdf = await this.stateService.getKdfType(); const kdfConfig = await this.stateService.getKdfConfig(); @@ -213,11 +218,13 @@ export class LockComponent implements OnInit, OnDestroy { kdf, kdfConfig, ); - const storedPasswordHash = await this.cryptoService.getMasterKeyHash(); + const storedMasterKeyHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); let passwordValid = false; - if (storedPasswordHash != null) { + if (storedMasterKeyHash != null) { // Offline unlock possible passwordValid = await this.cryptoService.compareAndUpdateKeyHash( this.masterPassword, @@ -242,7 +249,7 @@ export class LockComponent implements OnInit, OnDestroy { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); } catch (e) { this.logService.error(e); } finally { @@ -260,7 +267,7 @@ export class LockComponent implements OnInit, OnDestroy { } const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); await this.setUserKeyAndContinue(userKey, true); } @@ -269,7 +276,8 @@ export class LockComponent implements OnInit, OnDestroy { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device - await this.deviceTrustCryptoService.trustDeviceIfRequired(); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); await this.doContinue(evaluatePasswordAfterUnlock); } @@ -289,8 +297,10 @@ export class LockComponent implements OnInit, OnDestroy { } if (this.requirePasswordChange()) { - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.WeakMasterPassword, + userId, ); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index 45d7f563f7..5a1180cd38 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -1,17 +1,18 @@ import { Directive, OnDestroy, OnInit } from "@angular/core"; import { IsActiveMatchOptions, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, takeUntil } from "rxjs"; import { AuthRequestLoginCredentials, AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; @@ -32,6 +33,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -68,7 +70,6 @@ export class LoginViaAuthRequestComponent private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array }; - // TODO: in future, go to child components and remove child constructors and let deps fall through to the super class constructor( protected router: Router, private cryptoService: CryptoService, @@ -84,10 +85,11 @@ export class LoginViaAuthRequestComponent private anonymousHubService: AnonymousHubService, private validationService: ValidationService, private stateService: StateService, - private loginService: LoginService, + private loginEmailService: LoginEmailServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, + private accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); @@ -95,17 +97,19 @@ export class LoginViaAuthRequestComponent // Why would the existence of the email depend on the navigation? const navigation = this.router.getCurrentNavigation(); if (navigation) { - this.email = this.loginService.getEmail(); + this.email = this.loginEmailService.getEmail(); } - //gets signalR push notification - this.loginStrategyService.authRequestPushNotification$ + // Gets signalR push notification + // Only fires on approval to prevent enumeration + this.authRequestService.authRequestPushNotification$ .pipe(takeUntil(this.destroy$)) .subscribe((id) => { - // Only fires on approval currently - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.verifyAndHandleApprovedAuthReq(id); + this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => { + this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message); + this.logService.error("Failed to use approved auth request: " + e.message); + }); }); } @@ -128,6 +132,7 @@ export class LoginViaAuthRequestComponent // This also prevents it from being lost on refresh as the // login service email does not persist. this.email = await this.stateService.getEmail(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; if (!this.email) { this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); @@ -139,10 +144,10 @@ export class LoginViaAuthRequestComponent // We only allow a single admin approval request to be active at a time // so must check state to see if we have an existing one or not - const adminAuthReqStorable = await this.stateService.getAdminAuthRequest(); + const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId); if (adminAuthReqStorable) { - await this.handleExistingAdminAuthRequest(adminAuthReqStorable); + await this.handleExistingAdminAuthRequest(adminAuthReqStorable, userId); } else { // No existing admin auth request; so we need to create one await this.startAuthRequestLogin(); @@ -150,7 +155,7 @@ export class LoginViaAuthRequestComponent } else { // Standard auth request // TODO: evaluate if we can remove the setting of this.email in the constructor - this.email = this.loginService.getEmail(); + this.email = this.loginEmailService.getEmail(); if (!this.email) { this.platformUtilsService.showToast("error", null, this.i18nService.t("userEmailMissing")); @@ -164,13 +169,16 @@ export class LoginViaAuthRequestComponent } } - ngOnDestroy(): void { + async ngOnDestroy() { + await this.anonymousHubService.stopHubConnection(); this.destroy$.next(); this.destroy$.complete(); - this.anonymousHubService.stopHubConnection(); } - private async handleExistingAdminAuthRequest(adminAuthReqStorable: AdminAuthRequestStorable) { + private async handleExistingAdminAuthRequest( + adminAuthReqStorable: AdminAuthRequestStorable, + userId: UserId, + ) { // Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req // has been approved and handle it if so. @@ -180,13 +188,13 @@ export class LoginViaAuthRequestComponent adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id); } catch (error) { if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(); + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } } // Request doesn't exist anymore if (!adminAuthReqResponse) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(); + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } // Re-derive the user's fingerprint phrase @@ -200,7 +208,7 @@ export class LoginViaAuthRequestComponent // Request denied if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) { - return await this.handleExistingAdminAuthReqDeletedOrDenied(); + return await this.handleExistingAdminAuthReqDeletedOrDenied(userId); } // Request approved @@ -208,17 +216,18 @@ export class LoginViaAuthRequestComponent return await this.handleApprovedAdminAuthRequest( adminAuthReqResponse, adminAuthReqStorable.privateKey, + userId, ); } // Request still pending response from admin // So, create hub connection so that any approvals will be received via push notification - this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); + await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); } - private async handleExistingAdminAuthReqDeletedOrDenied() { + private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) { // clear the admin auth request from state - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); // start new auth request // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -266,14 +275,15 @@ export class LoginViaAuthRequestComponent privateKey: this.authRequestKeyPair.privateKey, }); - await this.stateService.setAdminAuthRequest(adminAuthReqStorable); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId); } else { await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock); reqResponse = await this.apiService.postAuthRequest(this.authRequest); } if (reqResponse.id) { - this.anonymousHubService.createHubConnection(reqResponse.id); + await this.anonymousHubService.createHubConnection(reqResponse.id); } } catch (e) { this.logService.error(e); @@ -330,9 +340,11 @@ export class LoginViaAuthRequestComponent // if user has authenticated via SSO if (this.userAuthNStatus === AuthenticationStatus.Locked) { + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; return await this.handleApprovedAdminAuthRequest( authReqResponse, this.authRequestKeyPair.privateKey, + userId, ); } @@ -360,6 +372,7 @@ export class LoginViaAuthRequestComponent async handleApprovedAdminAuthRequest( adminAuthReqResponse: AuthRequestResponse, privateKey: ArrayBuffer, + userId: UserId, ) { // See verifyAndHandleApprovedAuthReq(...) for flow details // it's flow 2 or 3 based on presence of masterPasswordHash @@ -381,13 +394,14 @@ export class LoginViaAuthRequestComponent // clear the admin auth request from state so it cannot be used again (it's a one time use) // TODO: this should eventually be enforced via deleting this on the server once it is used - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved")); // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device - await this.deviceTrustCryptoService.trustDeviceIfRequired(); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); // TODO: don't forget to use auto enrollment service everywhere we trust device @@ -471,17 +485,10 @@ export class LoginViaAuthRequestComponent } } - async setRememberEmailValues() { - const rememberEmail = this.loginService.getRememberEmail(); - const rememberedEmail = this.loginService.getEmail(); - await this.stateService.setRememberedEmail(rememberEmail ? rememberedEmail : null); - this.loginService.clearValues(); - } - private async handleSuccessfulLoginNavigation() { if (this.state === State.StandardAuthRequest) { // Only need to set remembered email on standard login with auth req flow - await this.setRememberEmailValues(); + await this.loginEmailService.saveEmailSettings(); } if (this.onSuccessfulLogin != null) { diff --git a/libs/angular/src/auth/components/login.component.ts b/libs/angular/src/auth/components/login.component.ts index 217d331198..bcdf747406 100644 --- a/libs/angular/src/auth/components/login.component.ts +++ b/libs/angular/src/auth/components/login.component.ts @@ -4,9 +4,12 @@ import { ActivatedRoute, Router } from "@angular/router"; import { Subject, firstValueFrom } from "rxjs"; import { take, takeUntil } from "rxjs/operators"; -import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, + PasswordLoginCredentials, +} from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -77,7 +80,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, protected formBuilder: FormBuilder, protected formValidationErrorService: FormValidationErrorsService, protected route: ActivatedRoute, - protected loginService: LoginService, + protected loginEmailService: LoginEmailServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected webAuthnLoginService: WebAuthnLoginServiceAbstraction, ) { @@ -93,25 +96,23 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, const queryParamsEmail = params.email; if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) { - this.formGroup.get("email").setValue(queryParamsEmail); - this.loginService.setEmail(queryParamsEmail); + this.formGroup.controls.email.setValue(queryParamsEmail); this.paramEmailSet = true; } }); - let email = this.loginService.getEmail(); - - if (email == null || email === "") { - email = await this.stateService.getRememberedEmail(); - } if (!this.paramEmailSet) { - this.formGroup.get("email")?.setValue(email ?? ""); + const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); + this.formGroup.controls.email.setValue(storedEmail ?? ""); } - let rememberEmail = this.loginService.getRememberEmail(); + + let rememberEmail = this.loginEmailService.getRememberEmail(); + if (rememberEmail == null) { - rememberEmail = (await this.stateService.getRememberedEmail()) != null; + rememberEmail = (await firstValueFrom(this.loginEmailService.storedEmail$)) != null; } - this.formGroup.get("rememberEmail")?.setValue(rememberEmail); + + this.formGroup.controls.rememberEmail.setValue(rememberEmail); } ngOnDestroy() { @@ -148,8 +149,10 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, this.formPromise = this.loginStrategyService.logIn(credentials); const response = await this.formPromise; - this.setFormValues(); - await this.loginService.saveEmailSettings(); + + this.setLoginEmailValues(); + await this.loginEmailService.saveEmailSettings(); + if (this.handleCaptchaRequired(response)) { return; } else if (this.handleMigrateEncryptionKey(response)) { @@ -214,7 +217,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, return; } - this.setFormValues(); + this.setLoginEmailValues(); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/login-with-device"]); @@ -292,14 +295,14 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit, } } - setFormValues() { - this.loginService.setEmail(this.formGroup.value.email); - this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); + setLoginEmailValues() { + this.loginEmailService.setEmail(this.formGroup.value.email); + this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); } async saveEmailSettings() { - this.setFormValues(); - await this.loginService.saveEmailSettings(); + this.setLoginEmailValues(); + await this.loginEmailService.saveEmailSettings(); // Save off email for SSO await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index a7442f711b..eebf87655b 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -12,6 +12,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; @@ -29,6 +31,7 @@ import { import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -45,11 +48,14 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { resetPasswordAutoEnroll = false; onSuccessfulChangePassword: () => Promise; successRoute = "vault"; + userId: UserId; forceSetPasswordReason: ForceSetPasswordReason = ForceSetPasswordReason.None; ForceSetPasswordReason = ForceSetPasswordReason; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, i18nService: I18nService, cryptoService: CryptoService, messagingService: MessagingService, @@ -88,7 +94,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.syncService.fullSync(true); this.syncLoading = false; - this.forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); + this.userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + + this.forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(this.userId), + ); this.route.queryParams .pipe( @@ -176,7 +186,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { if (response == null) { throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); } - const userId = await this.stateService.getUserId(); const publicKey = Utils.fromB64ToArray(response.publicKey); // RSA Encrypt user key with organization public key @@ -189,7 +198,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { return this.organizationUserService.putOrganizationUserResetPasswordEnrollment( this.orgId, - userId, + this.userId, resetRequest, ); }); @@ -226,7 +235,10 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { keyPair: [string, EncString] | null, ) { // Clear force set password reason to allow navigation back to vault. - await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.None, + this.userId, + ); // User now has a password so update account decryption options in state const userDecryptionOpts = await firstValueFrom( @@ -237,7 +249,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.stateService.setKdfType(this.kdf); await this.stateService.setKdfConfig(this.kdfConfig); - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, this.userId); await this.cryptoService.setUserKey(userKey[0]); // Set private key only for new JIT provisioned users in MP encryption orgs @@ -255,6 +267,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { masterKey, HashPurpose.LocalAuthorization, ); - await this.cryptoService.setMasterKeyHash(localMasterKeyHash); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, this.userId); } } diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 82650cb7f1..269ec51e30 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -12,18 +12,23 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { SsoComponent } from "./sso.component"; // test component that extends the SsoComponent @@ -48,6 +53,7 @@ describe("SsoComponent", () => { let component: TestSsoComponent; let _component: SsoComponentProtected; let fixture: ComponentFixture; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy; @@ -66,7 +72,9 @@ describe("SsoComponent", () => { let mockPasswordGenerationService: MockProxy; let mockLogService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; // Mock authService.logIn params let code: string; @@ -107,16 +115,18 @@ describe("SsoComponent", () => { queryParams: mockQueryParams, } as any as ActivatedRoute; - mockSsoLoginService = mock(); - mockStateService = mock(); - mockPlatformUtilsService = mock(); - mockApiService = mock(); - mockCryptoFunctionService = mock(); - mockEnvironmentService = mock(); - mockPasswordGenerationService = mock(); - mockLogService = mock(); - mockUserDecryptionOptionsService = mock(); - mockConfigService = mock(); + mockSsoLoginService = mock(); + mockStateService = mock(); + mockPlatformUtilsService = mock(); + mockApiService = mock(); + mockCryptoFunctionService = mock(); + mockEnvironmentService = mock(); + mockPasswordGenerationService = mock(); + mockLogService = mock(); + mockUserDecryptionOptionsService = mock(); + mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); // Mock loginStrategyService.logIn params code = "code"; @@ -198,7 +208,9 @@ describe("SsoComponent", () => { useValue: mockUserDecryptionOptionsService, }, { provide: LogService, useValue: mockLogService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -365,8 +377,9 @@ describe("SsoComponent", () => { await _component.logIn(code, codeVerifier, orgIdFromState); expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1); - expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); expect(mockOnSuccessfulLoginTdeNavigate).not.toHaveBeenCalled(); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index f7d5504e08..30815beef8 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -11,11 +11,13 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -65,7 +67,9 @@ export class SsoComponent { protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected logService: LogService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) {} async ngOnInit() { @@ -290,8 +294,10 @@ export class SsoComponent { // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index c27ba7082f..0eb248f6d9 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -7,26 +7,31 @@ import { BehaviorSubject } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { - FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption, LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, + FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption, FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption, FakeUserDecryptionOptions as UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { TwoFactorComponent } from "./two-factor.component"; @@ -46,6 +51,7 @@ describe("TwoFactorComponent", () => { let _component: TwoFactorComponentProtected; let fixture: ComponentFixture; + const userId = "userId" as UserId; // Mock Services let mockLoginStrategyService: MockProxy; @@ -59,10 +65,12 @@ describe("TwoFactorComponent", () => { let mockLogService: MockProxy; let mockTwoFactorService: MockProxy; let mockAppIdService: MockProxy; - let mockLoginService: MockProxy; + let mockLoginEmailService: MockProxy; let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; + let mockMasterPasswordService: FakeMasterPasswordService; + let mockAccountService: FakeAccountService; let mockUserDecryptionOpts: { noMasterPassword: UserDecryptionOptions; @@ -89,10 +97,12 @@ describe("TwoFactorComponent", () => { mockLogService = mock(); mockTwoFactorService = mock(); mockAppIdService = mock(); - mockLoginService = mock(); + mockLoginEmailService = mock(); mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); - mockConfigService = mock(); + mockConfigService = mock(); + mockAccountService = mockAccountServiceWith(userId); + mockMasterPasswordService = new FakeMasterPasswordService(); mockUserDecryptionOpts = { noMasterPassword: new UserDecryptionOptions({ @@ -163,13 +173,15 @@ describe("TwoFactorComponent", () => { { provide: LogService, useValue: mockLogService }, { provide: TwoFactorService, useValue: mockTwoFactorService }, { provide: AppIdService, useValue: mockAppIdService }, - { provide: LoginService, useValue: mockLoginService }, + { provide: LoginEmailServiceAbstraction, useValue: mockLoginEmailService }, { provide: UserDecryptionOptionsServiceAbstraction, useValue: mockUserDecryptionOptionsService, }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, + { provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService }, + { provide: AccountService, useValue: mockAccountService }, ], }); @@ -280,11 +292,11 @@ describe("TwoFactorComponent", () => { expect(component.onSuccessfulLogin).toHaveBeenCalled(); }); - it("calls loginService.clearValues() when login is successful", async () => { + it("calls loginEmailService.clearValues() when login is successful", async () => { // Arrange mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult()); - // spy on loginService.clearValues - const clearValuesSpy = jest.spyOn(mockLoginService, "clearValues"); + // spy on loginEmailService.clearValues + const clearValuesSpy = jest.spyOn(mockLoginEmailService, "clearValues"); // Act await component.doSubmit(); @@ -407,9 +419,9 @@ describe("TwoFactorComponent", () => { await component.doSubmit(); // Assert - - expect(mockStateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(mockMasterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); expect(mockRouter.navigate).toHaveBeenCalledTimes(1); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 78d1c020b8..f73f0483be 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -8,12 +8,14 @@ import { first } from "rxjs/operators"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, TrustedDeviceUserDecryptionOption, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -25,7 +27,7 @@ import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -88,10 +90,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected logService: LogService, protected twoFactorService: TwoFactorService, protected appIdService: AppIdService, - protected loginService: LoginService, + protected loginEmailService: LoginEmailServiceAbstraction, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, + protected accountService: AccountService, ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -288,7 +292,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI // - TDE login decryption options component // - Browser SSO on extension open await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier); - this.loginService.clearValues(); + this.loginEmailService.clearValues(); // note: this flow affects both TDE & standard users if (this.isForcePasswordResetRequired(authResult)) { @@ -342,8 +346,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 0b4541fe52..54fdc83239 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -1,9 +1,12 @@ import { Directive } from "@angular/core"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -56,6 +59,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { super( i18nService, @@ -72,7 +77,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async ngOnInit() { await this.syncService.fullSync(true); - this.reason = await this.stateService.getForceSetPasswordReason(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + this.reason = await firstValueFrom(this.masterPasswordService.forceSetPasswordReason$(userId)); // If we somehow end up here without a reason, go back to the home page if (this.reason == ForceSetPasswordReason.None) { @@ -163,7 +169,11 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { this.i18nService.t("updatedMasterPassword"), ); - await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.None, + userId, + ); if (this.onSuccessfulChangePassword != null) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index 29024cfa0b..b8e37d0af3 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -1,12 +1,14 @@ import { Injectable } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @Injectable() export class AuthGuard implements CanActivate { @@ -15,7 +17,8 @@ export class AuthGuard implements CanActivate { private router: Router, private messagingService: MessagingService, private keyConnectorService: KeyConnectorService, - private stateService: StateService, + private accountService: AccountService, + private masterPasswordService: MasterPasswordServiceAbstraction, ) {} async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { @@ -40,7 +43,10 @@ export class AuthGuard implements CanActivate { return this.router.createUrlTree(["/remove-password"]); } - const forceSetPasswordReason = await this.stateService.getForceSetPasswordReason(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(userId), + ); if ( forceSetPasswordReason === diff --git a/libs/angular/src/auth/guards/unauth.guard.ts b/libs/angular/src/auth/guards/unauth.guard.ts index 35c59b5744..9e1bca98ca 100644 --- a/libs/angular/src/auth/guards/unauth.guard.ts +++ b/libs/angular/src/auth/guards/unauth.guard.ts @@ -2,7 +2,6 @@ import { Injectable, inject } from "@angular/core"; import { CanActivate, CanActivateFn, Router, UrlTree } from "@angular/router"; import { Observable, map } from "rxjs"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -43,14 +42,14 @@ const defaultRoutes: UnauthRoutes = { }; function unauthGuard(routes: UnauthRoutes): Observable { - const accountService = inject(AccountService); + const authService = inject(AuthService); const router = inject(Router); - return accountService.activeAccount$.pipe( - map((accountData) => { - if (accountData == null || accountData.status === AuthenticationStatus.LoggedOut) { + return authService.activeAccountStatus$.pipe( + map((status) => { + if (status == null || status === AuthenticationStatus.LoggedOut) { return true; - } else if (accountData.status === AuthenticationStatus.Locked) { + } else if (status === AuthenticationStatus.Locked) { return router.createUrlTree([routes.locked]); } else { return router.createUrlTree([routes.homepage()]); diff --git a/libs/angular/src/auth/icons/create-passkey-failed.icon.ts b/libs/angular/src/auth/icons/create-passkey-failed.icon.ts index 39a2389c5a..65902a64c9 100644 --- a/libs/angular/src/auth/icons/create-passkey-failed.icon.ts +++ b/libs/angular/src/auth/icons/create-passkey-failed.icon.ts @@ -2,27 +2,27 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyFailedIcon = svgIcon` - - - - - - - - - `; diff --git a/libs/angular/src/auth/icons/create-passkey.icon.ts b/libs/angular/src/auth/icons/create-passkey.icon.ts index c0e984bbee..79ba4021b5 100644 --- a/libs/angular/src/auth/icons/create-passkey.icon.ts +++ b/libs/angular/src/auth/icons/create-passkey.icon.ts @@ -2,25 +2,25 @@ import { svgIcon } from "@bitwarden/components"; export const CreatePasskeyIcon = svgIcon` - - - - - - - - `; diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 53f064d6f4..6687e784f0 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -62,6 +62,7 @@ export class ShareComponent implements OnInit, OnDestroy { this.organizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { if (this.organizationId == null && orgs.length > 0) { this.organizationId = orgs[0].id; + this.filterCollections(); } }); @@ -69,8 +70,6 @@ export class ShareComponent implements OnInit, OnDestroy { this.cipher = await cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain), ); - - this.filterCollections(); } filterCollections() { diff --git a/libs/angular/src/directives/if-feature.directive.spec.ts b/libs/angular/src/directives/if-feature.directive.spec.ts index 01364b2ada..944410be7d 100644 --- a/libs/angular/src/directives/if-feature.directive.spec.ts +++ b/libs/angular/src/directives/if-feature.directive.spec.ts @@ -4,7 +4,7 @@ import { By } from "@angular/platform-browser"; import { mock, MockProxy } from "jest-mock-extended"; import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { IfFeatureDirective } from "./if-feature.directive"; @@ -39,7 +39,7 @@ class TestComponent { describe("IfFeatureDirective", () => { let fixture: ComponentFixture; let content: HTMLElement; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => { mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) => @@ -51,14 +51,14 @@ describe("IfFeatureDirective", () => { fixture.debugElement.query(By.css(`[data-testid="${testId}"]`))?.nativeElement; beforeEach(async () => { - mockConfigService = mock(); + mockConfigService = mock(); await TestBed.configureTestingModule({ declarations: [IfFeatureDirective, TestComponent], providers: [ { provide: LogService, useValue: mock() }, { - provide: ConfigServiceAbstraction, + provide: ConfigService, useValue: mockConfigService, }, ], diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index ff08125678..069f306a89 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -1,7 +1,7 @@ import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; /** @@ -30,7 +30,7 @@ export class IfFeatureDirective implements OnInit { constructor( private templateRef: TemplateRef, private viewContainer: ViewContainerRef, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private logService: LogService, ) {} diff --git a/libs/angular/src/platform/abstractions/form-validation-errors.service.ts b/libs/angular/src/platform/abstractions/form-validation-errors.service.ts index 08a12443a0..266afff5f3 100644 --- a/libs/angular/src/platform/abstractions/form-validation-errors.service.ts +++ b/libs/angular/src/platform/abstractions/form-validation-errors.service.ts @@ -9,5 +9,5 @@ export interface FormGroupControls { } export abstract class FormValidationErrorsService { - getFormValidationErrors: (controls: FormGroupControls) => AllValidationErrors[]; + abstract getFormValidationErrors(controls: FormGroupControls): AllValidationErrors[]; } diff --git a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts index 95dd56cd50..88637dff97 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts @@ -5,7 +5,7 @@ import { RouterTestingModule } from "@angular/router/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -21,11 +21,11 @@ describe("canAccessFeature", () => { const featureRoute = "enabled-feature"; const redirectRoute = "redirect"; - let mockConfigService: MockProxy; + let mockConfigService: MockProxy; let mockPlatformUtilsService: MockProxy; const setup = (featureGuard: CanActivateFn, flagValue: any) => { - mockConfigService = mock(); + mockConfigService = mock(); mockPlatformUtilsService = mock(); // Mock the correct getter based on the type of flagValue; also mock default values if one is not provided @@ -56,7 +56,7 @@ describe("canAccessFeature", () => { ]), ], providers: [ - { provide: ConfigServiceAbstraction, useValue: mockConfigService }, + { provide: ConfigService, useValue: mockConfigService }, { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, { provide: LogService, useValue: mock() }, { diff --git a/libs/angular/src/platform/guard/feature-flag.guard.ts b/libs/angular/src/platform/guard/feature-flag.guard.ts index 8842f04152..bfcabc2b53 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.ts @@ -2,7 +2,7 @@ import { inject } from "@angular/core"; import { CanActivateFn, Router } from "@angular/router"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -23,7 +23,7 @@ export const canAccessFeature = ( redirectUrlOnDisabled?: string, ): CanActivateFn => { return async () => { - const configService = inject(ConfigServiceAbstraction); + const configService = inject(ConfigService); const platformUtilsService = inject(PlatformUtilsService); const router = inject(Router); const i18nService = inject(I18nService); diff --git a/libs/angular/src/platform/services/logging-error-handler.ts b/libs/angular/src/platform/services/logging-error-handler.ts new file mode 100644 index 0000000000..522412dd28 --- /dev/null +++ b/libs/angular/src/platform/services/logging-error-handler.ts @@ -0,0 +1,22 @@ +import { ErrorHandler, Injectable, Injector, inject } from "@angular/core"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +@Injectable() +export class LoggingErrorHandler extends ErrorHandler { + /** + * When injecting services into an `ErrorHandler`, we must use the `Injector` manually to avoid circular dependency errors. + * + * https://stackoverflow.com/a/57115053 + */ + private injector = inject(Injector); + + override handleError(error: any): void { + try { + const logService = this.injector.get(LogService, null); + logService.error(error); + } catch { + super.handleError(error); + } + } +} diff --git a/libs/angular/src/platform/services/theming/theming.service.abstraction.ts b/libs/angular/src/platform/services/theming/theming.service.abstraction.ts index 9a012a7f75..4306d312c5 100644 --- a/libs/angular/src/platform/services/theming/theming.service.abstraction.ts +++ b/libs/angular/src/platform/services/theming/theming.service.abstraction.ts @@ -11,12 +11,12 @@ export abstract class AbstractThemingService { * The effective theme based on the user configured choice and the current system theme if * the configured choice is {@link ThemeType.System}. */ - theme$: Observable; + abstract theme$: Observable; /** * Listens for effective theme changes and applies changes to the provided document. * @param document The document that should have theme classes applied to it. * * @returns A subscription that can be unsubscribed from to cancel the application of theme classes. */ - applyThemeChangesTo: (document: Document) => Subscription; + abstract applyThemeChangesTo(document: Document): Subscription; } diff --git a/libs/angular/src/platform/utils/safe-provider.ts b/libs/angular/src/platform/utils/safe-provider.ts index 65ce49cda9..e7547f9b82 100644 --- a/libs/angular/src/platform/utils/safe-provider.ts +++ b/libs/angular/src/platform/utils/safe-provider.ts @@ -4,7 +4,7 @@ import { Constructor, Opaque } from "type-fest"; import { SafeInjectionToken } from "../../services/injection-tokens"; /** - * The return type of our dependency helper functions. + * The return type of the {@link safeProvider} helper function. * Used to distinguish a type safe provider definition from a non-type safe provider definition. */ export type SafeProvider = Opaque; @@ -18,12 +18,22 @@ type MapParametersToDeps = { type SafeInjectionTokenType = T extends SafeInjectionToken ? J : never; +/** + * Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken + */ +type ProviderInstanceType = + T extends SafeInjectionToken + ? InstanceType> + : T extends Constructor | AbstractConstructor + ? InstanceType + : never; + /** * Represents a dependency provided with the useClass option. */ type SafeClassProvider< - A extends AbstractConstructor, - I extends Constructor>, + A extends AbstractConstructor | SafeInjectionToken, + I extends Constructor>, D extends MapParametersToDeps>, > = { provide: A; @@ -40,75 +50,89 @@ type SafeValueProvider, V extends SafeInjectio }; /** - * Represents a dependency provided with the useFactory option where a SafeInjectionToken is used as the token. + * Represents a dependency provided with the useFactory option. */ -type SafeFactoryProviderWithToken< - A extends SafeInjectionToken, - I extends (...args: any) => InstanceType>, - D extends MapParametersToDeps>, -> = { - provide: A; - useFactory: I; - deps: D; -}; - -/** - * Represents a dependency provided with the useFactory option where an abstract class is used as the token. - */ -type SafeFactoryProviderWithClass< - A extends AbstractConstructor, - I extends (...args: any) => InstanceType, +type SafeFactoryProvider< + A extends AbstractConstructor | SafeInjectionToken, + I extends (...args: any) => ProviderInstanceType, D extends MapParametersToDeps>, > = { provide: A; useFactory: I; deps: D; + multi?: boolean; }; /** * Represents a dependency provided with the useExisting option. */ type SafeExistingProvider< - A extends Constructor | AbstractConstructor, - I extends Constructor> | AbstractConstructor>, + A extends Constructor | AbstractConstructor | SafeInjectionToken, + I extends Constructor> | AbstractConstructor>, > = { provide: A; useExisting: I; }; +/** + * Represents a dependency where there is no abstract token, the token is the implementation + */ +type SafeConcreteProvider< + I extends Constructor, + D extends MapParametersToDeps>, +> = { + provide: I; + deps: D; +}; + +/** + * If useAngularDecorators: true is specified, do not require a deps array. + * This is a manual override for where @Injectable decorators are used + */ +type UseAngularDecorators = Omit & { + useAngularDecorators: true; +}; + +/** + * Represents a type with a deps array that may optionally be overridden with useAngularDecorators + */ +type AllowAngularDecorators = T | UseAngularDecorators; + /** * A factory function that creates a provider for the ngModule providers array. - * This guarantees type safety for your provider definition. It does nothing at runtime. + * This (almost) guarantees type safety for your provider definition. It does nothing at runtime. + * Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator, + * however this cannot be enforced by the type system and will not cause an error if the decorator is not used. + * @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] }) * @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.) * @returns The exact same object without modification (pass-through). */ export const safeProvider = < // types for useClass - AClass extends AbstractConstructor, - IClass extends Constructor>, + AClass extends AbstractConstructor | SafeInjectionToken, + IClass extends Constructor>, DClass extends MapParametersToDeps>, // types for useValue AValue extends SafeInjectionToken, VValue extends SafeInjectionTokenType, - // types for useFactoryWithToken - AFactoryToken extends SafeInjectionToken, - IFactoryToken extends (...args: any) => InstanceType>, - DFactoryToken extends MapParametersToDeps>, - // types for useFactoryWithClass - AFactoryClass extends AbstractConstructor, - IFactoryClass extends (...args: any) => InstanceType, - DFactoryClass extends MapParametersToDeps>, + // types for useFactory + AFactory extends AbstractConstructor | SafeInjectionToken, + IFactory extends (...args: any) => ProviderInstanceType, + DFactory extends MapParametersToDeps>, // types for useExisting - AExisting extends Constructor | AbstractConstructor, + AExisting extends Constructor | AbstractConstructor | SafeInjectionToken, IExisting extends - | Constructor> - | AbstractConstructor>, + | Constructor> + | AbstractConstructor>, + // types for no token + IConcrete extends Constructor, + DConcrete extends MapParametersToDeps>, >( provider: - | SafeClassProvider + | AllowAngularDecorators> | SafeValueProvider - | SafeFactoryProviderWithToken - | SafeFactoryProviderWithClass + | AllowAngularDecorators> | SafeExistingProvider + | AllowAngularDecorators> | Constructor, ): SafeProvider => provider as SafeProvider; diff --git a/libs/angular/src/platform/utils/safe-provider.type.spec.ts b/libs/angular/src/platform/utils/safe-provider.type.spec.ts new file mode 100644 index 0000000000..6fe6d0d0b6 --- /dev/null +++ b/libs/angular/src/platform/utils/safe-provider.type.spec.ts @@ -0,0 +1,111 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// This rule bans @ts-expect-error comments without explanation. In this file, we use it to test our types, and +// explanation is provided in header comments before each test. + +import { safeProvider } from "./safe-provider"; + +class FooFactory { + create() { + return "thing"; + } +} + +abstract class FooService { + createFoo: (str: string) => string; +} + +class DefaultFooService implements FooService { + constructor(private factory: FooFactory) {} + + createFoo(str: string) { + return str ?? this.factory.create(); + } +} + +class BarFactory { + create() { + return 5; + } +} + +abstract class BarService { + createBar: (num: number) => number; +} + +class DefaultBarService implements BarService { + constructor(private factory: BarFactory) {} + + createBar(num: number) { + return num ?? this.factory.create(); + } +} + +abstract class FooBarService {} + +class DefaultFooBarService { + constructor( + private fooFactory: FooFactory, + private barFactory: BarFactory, + ) {} +} + +// useClass happy path with deps +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + deps: [FooFactory], +}); + +// useClass happy path with useAngularDecorators +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + useAngularDecorators: true, +}); + +// useClass: expect error if implementation does not match abstraction +safeProvider({ + provide: FooService, + // @ts-expect-error + useClass: DefaultBarService, + deps: [BarFactory], +}); + +// useClass: expect error if deps type does not match +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + // @ts-expect-error + deps: [BarFactory], +}); + +// useClass: expect error if not enough deps specified +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + // @ts-expect-error + deps: [], +}); + +// useClass: expect error if too many deps specified +safeProvider({ + provide: FooService, + useClass: DefaultFooService, + // @ts-expect-error + deps: [FooFactory, BarFactory], +}); + +// useClass: expect error if deps are in the wrong order +safeProvider({ + provide: FooBarService, + useClass: DefaultFooBarService, + // @ts-expect-error + deps: [BarFactory, FooFactory], +}); + +// useClass: expect error if no deps specified and not using Angular decorators +// @ts-expect-error +safeProvider({ + provide: FooService, + useClass: DefaultFooService, +}); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index beda7cbf4f..ad0881a4b3 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,5 +1,4 @@ -import { LOCALE_ID, NgModule } from "@angular/core"; -import { UnwrapOpaque } from "type-fest"; +import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; import { AuthRequestServiceAbstraction, @@ -8,6 +7,8 @@ import { PinCryptoService, LoginStrategyServiceAbstraction, LoginStrategyService, + LoginEmailServiceAbstraction, + LoginEmailService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, UserDecryptionOptionsServiceAbstraction, @@ -37,6 +38,7 @@ import { InternalPolicyService, PolicyService as PolicyServiceAbstraction, } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; @@ -46,6 +48,7 @@ import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/comm import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { ProviderApiService } from "@bitwarden/common/admin-console/services/provider/provider-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; import { @@ -59,7 +62,10 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; -import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/login.service"; +import { + InternalMasterPasswordServiceAbstraction, + MasterPasswordServiceAbstraction, +} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; @@ -78,7 +84,7 @@ import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; -import { LoginService } from "@bitwarden/common/auth/services/login.service"; +import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -111,7 +117,7 @@ import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -135,7 +141,7 @@ import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; -import { ConfigService } from "@bitwarden/common/platform/services/config/config.service"; +import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; @@ -192,6 +198,8 @@ import { } from "@bitwarden/common/tools/password-strength"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { SendStateProvider as SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; +import { SendStateProvider as SendStateProviderAbstraction } from "@bitwarden/common/tools/send/services/send-state.provider.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; import { InternalSendService, @@ -239,6 +247,7 @@ import { UnauthGuard } from "../auth/guards/unauth.guard"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { BroadcasterService } from "../platform/services/broadcaster.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; +import { LoggingErrorHandler } from "../platform/services/logging-error-handler"; import { AngularThemingService } from "../platform/services/theming/angular-theming.service"; import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction"; import { safeProvider, SafeProvider } from "../platform/utils/safe-provider"; @@ -267,7 +276,7 @@ import { ModalService } from "./modal.service"; * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. * If you need help please ask for it, do NOT change the type of this array. */ -const typesafeProviders: Array = [ +const safeProviders: SafeProvider[] = [ safeProvider(AuthGuard), safeProvider(UnauthGuard), safeProvider(ModalService), @@ -345,16 +354,20 @@ const typesafeProviders: Array = [ provide: AuthServiceAbstraction, useClass: AuthService, deps: [ + AccountServiceAbstraction, MessagingServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, StateServiceAbstraction, + TokenServiceAbstraction, ], }), safeProvider({ provide: LoginStrategyServiceAbstraction, useClass: LoginStrategyService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -399,7 +412,8 @@ const typesafeProviders: Array = [ autofillSettingsService: AutofillSettingsServiceAbstraction, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, - configService: ConfigServiceAbstraction, + configService: ConfigService, + stateProvider: StateProvider, ) => new CipherService( cryptoService, @@ -412,6 +426,7 @@ const typesafeProviders: Array = [ encryptService, fileUploadService, configService, + stateProvider, ), deps: [ CryptoServiceAbstraction, @@ -423,7 +438,8 @@ const typesafeProviders: Array = [ AutofillSettingsServiceAbstraction, EncryptService, CipherFileUploadServiceAbstraction, - ConfigServiceAbstraction, + ConfigService, + StateProvider, ], }), safeProvider({ @@ -433,7 +449,6 @@ const typesafeProviders: Array = [ CryptoServiceAbstraction, I18nServiceAbstraction, CipherServiceAbstraction, - StateServiceAbstraction, StateProvider, ], }), @@ -502,7 +517,10 @@ const typesafeProviders: Array = [ SingleUserStateProvider, GlobalStateProvider, SUPPORTS_SECURE_STORAGE, - AbstractStorageService, + SECURE_STORAGE, + KeyGenerationServiceAbstraction, + EncryptService, + LogService, ], }), safeProvider({ @@ -514,6 +532,7 @@ const typesafeProviders: Array = [ provide: CryptoServiceAbstraction, useClass: CryptoService, deps: [ + InternalMasterPasswordServiceAbstraction, KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, EncryptService, @@ -562,9 +581,15 @@ const typesafeProviders: Array = [ CryptoServiceAbstraction, I18nServiceAbstraction, KeyGenerationServiceAbstraction, - StateServiceAbstraction, + SendStateProviderAbstraction, + EncryptService, ], }), + safeProvider({ + provide: SendStateProviderAbstraction, + useClass: SendStateProvider, + deps: [StateProvider], + }), safeProvider({ provide: SendApiServiceAbstraction, useClass: SendApiService, @@ -574,6 +599,8 @@ const typesafeProviders: Array = [ provide: SyncServiceAbstraction, useClass: SyncService, deps: [ + InternalMasterPasswordServiceAbstraction, + AccountServiceAbstraction, ApiServiceAbstraction, DomainSettingsService, InternalFolderService, @@ -613,6 +640,8 @@ const typesafeProviders: Array = [ provide: VaultTimeoutService, useClass: VaultTimeoutService, deps: [ + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, @@ -701,7 +730,7 @@ const typesafeProviders: Array = [ safeProvider({ provide: SearchServiceAbstraction, useClass: SearchService, - deps: [LogService, I18nServiceAbstraction], + deps: [LogService, I18nServiceAbstraction, StateProvider], }), safeProvider({ provide: NotificationsServiceAbstraction, @@ -715,6 +744,7 @@ const typesafeProviders: Array = [ LOGOUT_CALLBACK, StateServiceAbstraction, AuthServiceAbstraction, + AuthRequestServiceAbstraction, MessagingServiceAbstraction, ], }), @@ -731,7 +761,7 @@ const typesafeProviders: Array = [ safeProvider({ provide: EventUploadServiceAbstraction, useClass: EventUploadService, - deps: [ApiServiceAbstraction, StateProvider, LogService, AccountServiceAbstraction], + deps: [ApiServiceAbstraction, StateProvider, LogService, AuthServiceAbstraction], }), safeProvider({ provide: EventCollectionServiceAbstraction, @@ -741,7 +771,7 @@ const typesafeProviders: Array = [ StateProvider, OrganizationServiceAbstraction, EventUploadServiceAbstraction, - AccountServiceAbstraction, + AuthServiceAbstraction, ], }), safeProvider({ @@ -758,11 +788,21 @@ const typesafeProviders: Array = [ useClass: PolicyApiService, deps: [InternalPolicyService, ApiServiceAbstraction], }), + safeProvider({ + provide: InternalMasterPasswordServiceAbstraction, + useClass: MasterPasswordService, + deps: [StateProvider], + }), + safeProvider({ + provide: MasterPasswordServiceAbstraction, + useExisting: InternalMasterPasswordServiceAbstraction, + }), safeProvider({ provide: KeyConnectorServiceAbstraction, useClass: KeyConnectorService, deps: [ - StateServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, TokenServiceAbstraction, @@ -770,6 +810,7 @@ const typesafeProviders: Array = [ OrganizationServiceAbstraction, KeyGenerationServiceAbstraction, LOGOUT_CALLBACK, + StateProvider, ], }), safeProvider({ @@ -778,6 +819,8 @@ const typesafeProviders: Array = [ deps: [ StateServiceAbstraction, CryptoServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -847,30 +890,23 @@ const typesafeProviders: Array = [ deps: [], }), safeProvider({ - provide: ConfigService, - useClass: ConfigService, - deps: [ - StateServiceAbstraction, - ConfigApiServiceAbstraction, - AuthServiceAbstraction, - EnvironmentService, - LogService, - StateProvider, - ], + provide: DefaultConfigService, + useClass: DefaultConfigService, + deps: [ConfigApiServiceAbstraction, EnvironmentService, LogService, StateProvider], }), safeProvider({ - provide: ConfigServiceAbstraction, - useExisting: ConfigService, + provide: ConfigService, + useExisting: DefaultConfigService, }), safeProvider({ provide: ConfigApiServiceAbstraction, useClass: ConfigApiService, - deps: [ApiServiceAbstraction, AuthServiceAbstraction], + deps: [ApiServiceAbstraction, TokenServiceAbstraction], }), safeProvider({ provide: AnonymousHubServiceAbstraction, useClass: AnonymousHubService, - deps: [EnvironmentService, LoginStrategyServiceAbstraction, LogService], + deps: [EnvironmentService, AuthRequestServiceAbstraction], }), safeProvider({ provide: ValidationServiceAbstraction, @@ -878,9 +914,9 @@ const typesafeProviders: Array = [ deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], }), safeProvider({ - provide: LoginServiceAbstraction, - useClass: LoginService, - deps: [StateServiceAbstraction], + provide: LoginEmailServiceAbstraction, + useClass: LoginEmailService, + deps: [StateProvider], }), safeProvider({ provide: OrgDomainInternalServiceAbstraction, @@ -914,11 +950,12 @@ const typesafeProviders: Array = [ CryptoFunctionServiceAbstraction, CryptoServiceAbstraction, EncryptService, - StateServiceAbstraction, AppIdServiceAbstraction, DevicesApiServiceAbstraction, I18nServiceAbstraction, PlatformUtilsServiceAbstraction, + StateProvider, + SECURE_STORAGE, UserDecryptionOptionsServiceAbstraction, ], }), @@ -927,9 +964,11 @@ const typesafeProviders: Array = [ useClass: AuthRequestService, deps: [ AppIdServiceAbstraction, + AccountServiceAbstraction, + InternalMasterPasswordServiceAbstraction, CryptoServiceAbstraction, ApiServiceAbstraction, - StateServiceAbstraction, + StateProvider, ], }), safeProvider({ @@ -1066,13 +1105,23 @@ const typesafeProviders: Array = [ safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, - deps: [ActiveUserStateProvider], + deps: [StateProvider], }), safeProvider({ provide: OrganizationManagementPreferencesService, useClass: DefaultOrganizationManagementPreferencesService, deps: [StateProvider], }), + safeProvider({ + provide: ErrorHandler, + useClass: LoggingErrorHandler, + deps: [], + }), + safeProvider({ + provide: ProviderApiServiceAbstraction, + useClass: ProviderApiService, + deps: [ApiServiceAbstraction], + }), ]; function encryptServiceFactory( @@ -1088,6 +1137,6 @@ function encryptServiceFactory( @NgModule({ declarations: [], // Do not register your dependency here! Add it to the typesafeProviders array using the helper function - providers: typesafeProviders as UnwrapOpaque[], + providers: safeProviders, }) export class JslibServicesModule {} diff --git a/libs/angular/src/services/unassigned-items-banner.api.service.ts b/libs/angular/src/services/unassigned-items-banner.api.service.ts new file mode 100644 index 0000000000..69b74f8c7f --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.api.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; + +@Injectable({ providedIn: "root" }) +export class UnassignedItemsBannerApiService { + constructor(private apiService: ApiService) {} + + async getShowUnassignedCiphersBanner(): Promise { + const r = await this.apiService.send( + "GET", + "/ciphers/has-unassigned-ciphers", + null, + true, + true, + ); + return r; + } +} diff --git a/libs/angular/src/services/unassigned-items-banner.service.spec.ts b/libs/angular/src/services/unassigned-items-banner.service.spec.ts new file mode 100644 index 0000000000..bf0fb23881 --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.service.spec.ts @@ -0,0 +1,65 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; +import { SHOW_BANNER_KEY, UnassignedItemsBannerService } from "./unassigned-items-banner.service"; + +describe("UnassignedItemsBanner", () => { + let stateProvider: FakeStateProvider; + let apiService: MockProxy; + let environmentService: MockProxy; + let organizationService: MockProxy; + + const sutFactory = () => + new UnassignedItemsBannerService( + stateProvider, + apiService, + environmentService, + organizationService, + ); + + beforeEach(() => { + const fakeAccountService = mockAccountServiceWith("userId" as UserId); + stateProvider = new FakeStateProvider(fakeAccountService); + apiService = mock(); + environmentService = mock(); + environmentService.environment$ = of(null); + organizationService = mock(); + organizationService.organizations$ = of([]); + }); + + it("shows the banner if showBanner local state is true", async () => { + const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); + showBanner.nextState(true); + + const sut = sutFactory(); + expect(await firstValueFrom(sut.showBanner$)).toBe(true); + expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); + }); + + it("does not show the banner if showBanner local state is false", async () => { + const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); + showBanner.nextState(false); + + const sut = sutFactory(); + expect(await firstValueFrom(sut.showBanner$)).toBe(false); + expect(apiService.getShowUnassignedCiphersBanner).not.toHaveBeenCalled(); + }); + + it("fetches from server if local state has not been set yet", async () => { + apiService.getShowUnassignedCiphersBanner.mockResolvedValue(true); + + const showBanner = stateProvider.activeUser.getFake(SHOW_BANNER_KEY); + showBanner.nextState(undefined); + + const sut = sutFactory(); + + expect(await firstValueFrom(sut.showBanner$)).toBe(true); + expect(apiService.getShowUnassignedCiphersBanner).toHaveBeenCalledTimes(1); + }); +}); diff --git a/libs/angular/src/services/unassigned-items-banner.service.ts b/libs/angular/src/services/unassigned-items-banner.service.ts new file mode 100644 index 0000000000..db93d4c4fc --- /dev/null +++ b/libs/angular/src/services/unassigned-items-banner.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from "@angular/core"; +import { combineLatest, concatMap, map, startWith } from "rxjs"; + +import { + OrganizationService, + canAccessOrgAdmin, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { + StateProvider, + UNASSIGNED_ITEMS_BANNER_DISK, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; + +import { UnassignedItemsBannerApiService } from "./unassigned-items-banner.api.service"; + +export const SHOW_BANNER_KEY = new UserKeyDefinition( + UNASSIGNED_ITEMS_BANNER_DISK, + "showBanner", + { + deserializer: (b) => b, + clearOn: [], + }, +); + +/** Displays a banner that tells users how to move their unassigned items into a collection. */ +@Injectable({ providedIn: "root" }) +export class UnassignedItemsBannerService { + private _showBanner = this.stateProvider.getActive(SHOW_BANNER_KEY); + + showBanner$ = this._showBanner.state$.pipe( + concatMap(async (showBannerState) => { + // null indicates that the user has not seen or dismissed the banner yet - get the flag from server + if (showBannerState == null) { + const showBannerResponse = await this.apiService.getShowUnassignedCiphersBanner(); + await this._showBanner.update(() => showBannerResponse); + return showBannerResponse; + } + + return showBannerState; + }), + ); + + private adminConsoleOrg$ = this.organizationService.organizations$.pipe( + map((orgs) => orgs.find((o) => canAccessOrgAdmin(o))), + ); + + adminConsoleUrl$ = combineLatest([ + this.adminConsoleOrg$, + this.environmentService.environment$, + ]).pipe( + map(([org, environment]) => { + if (org == null || environment == null) { + return "#"; + } + + return environment.getWebVaultUrl() + "/#/organizations/" + org.id; + }), + ); + + bannerText$ = this.environmentService.environment$.pipe( + map((e) => + e?.getRegion() == Region.SelfHosted + ? "unassignedItemsBannerSelfHostNotice" + : "unassignedItemsBannerNotice", + ), + ); + + loading$ = combineLatest([this.adminConsoleUrl$, this.bannerText$]).pipe( + startWith(true), + map(() => false), + ); + + constructor( + private stateProvider: StateProvider, + private apiService: UnassignedItemsBannerApiService, + private environmentService: EnvironmentService, + private organizationService: OrganizationService, + ) {} + + async hideBanner() { + await this._showBanner.update(() => false); + } +} diff --git a/libs/angular/src/tools/generator/components/generator.component.ts b/libs/angular/src/tools/generator/components/generator.component.ts index d1c82a37b3..d1857a88ad 100644 --- a/libs/angular/src/tools/generator/components/generator.component.ts +++ b/libs/angular/src/tools/generator/components/generator.component.ts @@ -33,7 +33,7 @@ export class GeneratorComponent implements OnInit { subaddressOptions: any[]; catchallOptions: any[]; forwardOptions: EmailForwarderOptions[]; - usernameOptions: UsernameGeneratorOptions = {}; + usernameOptions: UsernameGeneratorOptions = { website: null }; passwordOptions: PasswordGeneratorOptions = {}; username = "-"; password = "-"; @@ -199,12 +199,12 @@ export class GeneratorComponent implements OnInit { } async sliderInput() { - this.normalizePasswordOptions(); + await this.normalizePasswordOptions(); this.password = await this.passwordGenerationService.generatePassword(this.passwordOptions); } async savePasswordOptions(regenerate = true) { - this.normalizePasswordOptions(); + await this.normalizePasswordOptions(); await this.passwordGenerationService.saveOptions(this.passwordOptions); if (regenerate && this.regenerateWithoutButtonPress()) { @@ -271,7 +271,7 @@ export class GeneratorComponent implements OnInit { return this.type !== "username" || this.usernameOptions.type !== "forwarded"; } - private normalizePasswordOptions() { + private async normalizePasswordOptions() { // Application level normalize options dependent on class variables this.passwordOptions.ambiguous = !this.avoidAmbiguous; @@ -290,9 +290,8 @@ export class GeneratorComponent implements OnInit { } } - this.passwordGenerationService.normalizeOptions( + await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions( this.passwordOptions, - this.enforcedPasswordPolicyOptions, ); this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength); diff --git a/libs/angular/src/tools/send/send.component.ts b/libs/angular/src/tools/send/send.component.ts index b3b871a177..fc51e32416 100644 --- a/libs/angular/src/tools/send/send.component.ts +++ b/libs/angular/src/tools/send/send.component.ts @@ -1,5 +1,13 @@ import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { + BehaviorSubject, + Subject, + firstValueFrom, + mergeMap, + from, + switchMap, + takeUntil, +} from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -24,7 +32,6 @@ export class SendComponent implements OnInit, OnDestroy { expired = false; type: SendType = null; sends: SendView[] = []; - searchText: string; selectedType: SendType; selectedAll: boolean; filter: (cipher: SendView) => boolean; @@ -39,6 +46,8 @@ export class SendComponent implements OnInit, OnDestroy { private searchTimeout: any; private destroy$ = new Subject(); private _filteredSends: SendView[]; + private _searchText$ = new BehaviorSubject(""); + protected isSearchable: boolean = false; get filteredSends(): SendView[] { return this._filteredSends; @@ -48,6 +57,14 @@ export class SendComponent implements OnInit, OnDestroy { this._filteredSends = filteredSends; } + get searchText() { + return this._searchText$.value; + } + + set searchText(value: string) { + this._searchText$.next(value); + } + constructor( protected sendService: SendService, protected i18nService: I18nService, @@ -68,6 +85,15 @@ export class SendComponent implements OnInit, OnDestroy { .subscribe((policyAppliesToActiveUser) => { this.disableSend = policyAppliesToActiveUser; }); + + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearchable = isSearchable; + }); } ngOnDestroy() { @@ -77,9 +103,15 @@ export class SendComponent implements OnInit, OnDestroy { async load(filter: (send: SendView) => boolean = null) { this.loading = true; - this.sendService.sendViews$.pipe(takeUntil(this.destroy$)).subscribe((sends) => { - this.sends = sends; - }); + this.sendService.sendViews$ + .pipe( + mergeMap(async (sends) => { + this.sends = sends; + await this.search(null); + }), + takeUntil(this.destroy$), + ) + .subscribe(); if (this.onSuccessfulLoad != null) { await this.onSuccessfulLoad(); } else { @@ -116,14 +148,14 @@ export class SendComponent implements OnInit, OnDestroy { clearTimeout(this.searchTimeout); } if (timeout == null) { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.applyTextSearch(); return; } this.searchPending = true; this.searchTimeout = setTimeout(async () => { - this.hasSearched = this.searchService.isSearchable(this.searchText); + this.hasSearched = this.isSearchable; this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s)); this.applyTextSearch(); this.searchPending = false; diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 680672514a..d29c74b42d 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -1,6 +1,6 @@ import { DatePipe } from "@angular/common"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { concatMap, Observable, Subject, takeUntil } from "rxjs"; +import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -14,7 +14,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; -import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -119,7 +119,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected dialogService: DialogService, protected win: Window, protected datePipe: DatePipe, - protected configService: ConfigServiceAbstraction, + protected configService: ConfigService, ) { this.typeOptions = [ { name: i18nService.t("typeLogin"), value: CipherType.Login }, @@ -402,6 +402,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } } + removePasskey() { + if (this.cipher.type !== CipherType.Login || this.cipher.login.fido2Credentials == null) { + return; + } + + this.cipher.login.fido2Credentials = null; + } + onCardNumberChange(): void { this.cipher.card.brand = CardView.getCardBrandByPatterns(this.cipher.card.number); } @@ -584,7 +592,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.writeableCollections.forEach((c) => ((c as any).checked = false)); } if (this.cipher.organizationId != null) { - this.collections = this.writeableCollections.filter( + this.collections = this.writeableCollections?.filter( (c) => c.organizationId === this.cipher.organizationId, ); const org = await this.organizationService.get(this.cipher.organizationId); @@ -650,11 +658,11 @@ export class AddEditComponent implements OnInit, OnDestroy { protected saveCipher(cipher: Cipher) { const isNotClone = this.editMode && !this.cloneMode; - let orgAdmin = this.organization?.isAdmin; + let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); - if (this.flexibleCollectionsV1Enabled) { - // Flexible Collections V1 restricts admins, check the organization setting via canEditAllCiphers - orgAdmin = this.organization?.canEditAllCiphers(true); + // if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection + if (!cipher.collectionIds) { + orgAdmin = this.organization?.canEditUnassignedCiphers(); } return this.cipher.id == null @@ -663,14 +671,14 @@ export class AddEditComponent implements OnInit, OnDestroy { } protected deleteCipher() { - const asAdmin = this.organization?.canEditAnyCollection; + const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id, asAdmin) : this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); } protected restoreCipher() { - const asAdmin = this.organization?.canEditAnyCollection; + const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); return this.cipherService.restoreWithServer(this.cipher.id, asAdmin); } @@ -679,7 +687,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } async loadAddEditCipherInfo(): Promise { - const addEditCipherInfo: any = await this.stateService.getAddEditCipherInfo(); + const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$); const loadedSavedInfo = addEditCipherInfo != null; if (loadedSavedInfo) { @@ -692,7 +700,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } } - await this.stateService.setAddEditCipherInfo(null); + await this.cipherService.setAddEditCipherInfo(null); return loadedSavedInfo; } diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index cdfb1b6299..20e779e77c 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,4 +1,5 @@ -import { Directive, EventEmitter, Input, Output } from "@angular/core"; +import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -6,7 +7,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Directive() -export class VaultItemsComponent { +export class VaultItemsComponent implements OnInit, OnDestroy { @Input() activeCipherId: string = null; @Output() onCipherClicked = new EventEmitter(); @Output() onCipherRightClicked = new EventEmitter(); @@ -23,13 +24,15 @@ export class VaultItemsComponent { protected searchPending = false; + private destroy$ = new Subject(); private searchTimeout: any = null; - private _searchText: string = null; + private isSearchable: boolean = false; + private _searchText$ = new BehaviorSubject(""); get searchText() { - return this._searchText; + return this._searchText$.value; } set searchText(value: string) { - this._searchText = value; + this._searchText$.next(value); } constructor( @@ -37,6 +40,22 @@ export class VaultItemsComponent { protected cipherService: CipherService, ) {} + ngOnInit(): void { + this._searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.isSearchable = isSearchable; + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + async load(filter: (cipher: CipherView) => boolean = null, deleted = false) { this.deleted = deleted ?? false; await this.applyFilter(filter); @@ -90,7 +109,7 @@ export class VaultItemsComponent { } isSearching() { - return !this.searchPending && this.searchService.isSearchable(this.searchText); + return !this.searchPending && this.isSearchable; } protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; diff --git a/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts b/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts index 1fb994fda1..f661f9330b 100644 --- a/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts +++ b/libs/auth/src/angular/icons/user-verification-biometrics-fingerprint.icon.ts @@ -2,11 +2,11 @@ import { svgIcon } from "@bitwarden/components"; export const UserVerificationBiometricsIcon = svgIcon` - - - - - - + + + + + + `; diff --git a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts index b91444d3e6..b7ae903eac 100644 --- a/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts +++ b/libs/auth/src/common/abstractions/auth-request.service.abstraction.ts @@ -1,7 +1,52 @@ +import { Observable } from "rxjs"; + +import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; export abstract class AuthRequestServiceAbstraction { + /** Emits an auth request id when an auth request has been approved. */ + authRequestPushNotification$: Observable; + + /** + * Returns true if the user has chosen to allow auth requests to show on this client. + * Intended to prevent spamming the user with auth requests. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract getAcceptAuthRequests: (userId: UserId) => Promise; + /** + * Sets whether to allow auth requests to show on this client for this user. + * @param accept Whether to allow auth requests to show on this client. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract setAcceptAuthRequests: (accept: boolean, userId: UserId) => Promise; + /** + * Returns an admin auth request for the given user if it exists. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract getAdminAuthRequest: (userId: UserId) => Promise; + /** + * Sets an admin auth request for the given user. + * Note: use {@link clearAdminAuthRequest} to clear the request. + * @param authRequest The admin auth request. + * @param userId The user id. + * @throws If `authRequest` or `userId` is not provided. + */ + abstract setAdminAuthRequest: ( + authRequest: AdminAuthRequestStorable, + userId: UserId, + ) => Promise; + /** + * Clears an admin auth request for the given user. + * @param userId The user id. + * @throws If `userId` is not provided. + */ + abstract clearAdminAuthRequest: (userId: UserId) => Promise; /** * Approve or deny an auth request. * @param approve True to approve, false to deny. @@ -54,4 +99,11 @@ export abstract class AuthRequestServiceAbstraction { pubKeyEncryptedMasterKeyHash: string, privateKey: ArrayBuffer, ) => Promise<{ masterKey: MasterKey; masterKeyHash: string }>; + + /** + * Handles incoming auth request push notifications. + * @param notification push notification. + * @remark We should only be receiving approved push notifications to prevent enumeration. + */ + abstract sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => void; } diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts index 1feee6695a..71280b72f6 100644 --- a/libs/auth/src/common/abstractions/index.ts +++ b/libs/auth/src/common/abstractions/index.ts @@ -1,4 +1,5 @@ export * from "./pin-crypto.service.abstraction"; +export * from "./login-email.service"; export * from "./login-strategy.service"; export * from "./user-decryption-options.service.abstraction"; export * from "./auth-request.service.abstraction"; diff --git a/libs/auth/src/common/abstractions/login-email.service.ts b/libs/auth/src/common/abstractions/login-email.service.ts new file mode 100644 index 0000000000..89165af543 --- /dev/null +++ b/libs/auth/src/common/abstractions/login-email.service.ts @@ -0,0 +1,38 @@ +import { Observable } from "rxjs"; + +export abstract class LoginEmailServiceAbstraction { + /** + * An observable that monitors the storedEmail + */ + storedEmail$: Observable; + /** + * Gets the current email being used in the login process. + * @returns A string of the email. + */ + getEmail: () => string; + /** + * Sets the current email being used in the login process. + * @param email The email to be set. + */ + setEmail: (email: string) => void; + /** + * Gets whether or not the email should be stored on disk. + * @returns A boolean stating whether or not the email should be stored on disk. + */ + getRememberEmail: () => boolean; + /** + * Sets whether or not the email should be stored on disk. + */ + setRememberEmail: (value: boolean) => void; + /** + * Sets the email and rememberEmail properties to null. + */ + clearValues: () => void; + /** + * - If rememberEmail is true, sets the storedEmail on disk to the current email. + * - If rememberEmail is false, sets the storedEmail on disk to null. + * - Then sets the email and rememberEmail properties to null. + * @returns A promise that resolves once the email settings are saved. + */ + saveEmailSettings: () => Promise; +} diff --git a/libs/auth/src/common/abstractions/login-strategy.service.ts b/libs/auth/src/common/abstractions/login-strategy.service.ts index e3ed63c737..eae6dc2a27 100644 --- a/libs/auth/src/common/abstractions/login-strategy.service.ts +++ b/libs/auth/src/common/abstractions/login-strategy.service.ts @@ -4,7 +4,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; -import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { MasterKey } from "@bitwarden/common/types/key"; import { @@ -21,10 +20,6 @@ export abstract class LoginStrategyServiceAbstraction { * Emits null if the session has timed out. */ currentAuthType$: Observable; - /** - * Emits when an auth request has been approved. - */ - authRequestPushNotification$: Observable; /** * If the login strategy uses the email address of the user, this * will return it. Otherwise, it will return null. @@ -77,10 +72,6 @@ export abstract class LoginStrategyServiceAbstraction { * Creates a master key from the provided master password and email. */ makePreloginKey: (masterPassword: string, email: string) => Promise; - /** - * Sends a notification to {@link LoginStrategyServiceAbstraction.authRequestPushNotification} - */ - sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => Promise; /** * Sends a response to an auth request. */ diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 53722cd259..0ce6c9fed7 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -5,6 +5,7 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -14,7 +15,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -42,6 +45,10 @@ describe("AuthRequestLoginStrategy", () => { let deviceTrustCryptoService: MockProxy; let billingAccountProfileStateService: MockProxy; + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let authRequestLoginStrategy: AuthRequestLoginStrategy; let credentials: AuthRequestLoginCredentials; let tokenResponse: IdentityTokenResponse; @@ -71,12 +78,17 @@ describe("AuthRequestLoginStrategy", () => { deviceTrustCryptoService = mock(); billingAccountProfileStateService = mock(); + accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); + tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.decodeAccessToken.mockResolvedValue({}); authRequestLoginStrategy = new AuthRequestLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -108,13 +120,16 @@ describe("AuthRequestLoginStrategy", () => { const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await authRequestLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(decMasterKeyHash); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, mockUserId); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + decMasterKeyHash, + mockUserId, + ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); @@ -136,8 +151,8 @@ describe("AuthRequestLoginStrategy", () => { await authRequestLoginStrategy.logIn(credentials); // setMasterKey and setMasterKeyHash should not be called - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); - expect(cryptoService.setMasterKeyHash).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled(); // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index c42f43e764..4035a7be58 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -1,8 +1,10 @@ -import { Observable, map, BehaviorSubject } from "rxjs"; +import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -16,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; @@ -46,6 +49,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { constructor( data: AuthRequestLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -60,6 +65,8 @@ export class AuthRequestLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -113,12 +120,22 @@ export class AuthRequestLoginStrategy extends LoginStrategy { authRequestCredentials.decryptedMasterKey && authRequestCredentials.decryptedMasterKeyHash ) { - await this.cryptoService.setMasterKey(authRequestCredentials.decryptedMasterKey); - await this.cryptoService.setMasterKeyHash(authRequestCredentials.decryptedMasterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey( + authRequestCredentials.decryptedMasterKey, + userId, + ); + await this.masterPasswordService.setMasterKeyHash( + authRequestCredentials.decryptedMasterKeyHash, + userId, + ); } } - protected override async setUserKey(response: IdentityTokenResponse): Promise { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { const authRequestCredentials = this.cache.value.authRequestCredentials; // User now may or may not have a master password // but set the master key encrypted user key if it exists regardless @@ -128,13 +145,15 @@ export class AuthRequestLoginStrategy extends LoginStrategy { await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey); } else { await this.trySetUserKeyWithMasterKey(); + // Establish trust if required after setting user key - await this.deviceTrustCryptoService.trustDeviceIfRequired(); + await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); } } private async trySetUserKeyWithMasterKey(): Promise { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index ed40797df5..431f736e94 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -14,6 +14,7 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -31,12 +32,14 @@ import { } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, } from "@bitwarden/common/tools/password-strength"; import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -56,7 +59,7 @@ const privateKey = "PRIVATE_KEY"; const captchaSiteKey = "CAPTCHA_SITE_KEY"; const kdf = 0; const kdfIterations = 10000; -const userId = Utils.newGuid(); +const userId = Utils.newGuid() as UserId; const masterPasswordHash = "MASTER_PASSWORD_HASH"; const name = "NAME"; const defaultUserDecryptionOptionsServerResponse: IUserDecryptionOptionsServerResponse = { @@ -98,6 +101,8 @@ export function identityTokenResponseFactory( // TODO: add tests for latest changes to base class for TDE describe("LoginStrategy", () => { let cache: PasswordLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; let cryptoService: MockProxy; @@ -118,6 +123,9 @@ describe("LoginStrategy", () => { let credentials: PasswordLoginCredentials; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock(); cryptoService = mock(); apiService = mock(); @@ -139,6 +147,8 @@ describe("LoginStrategy", () => { // The base class is abstract so we test it via PasswordLoginStrategy passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -186,9 +196,9 @@ describe("LoginStrategy", () => { expect(tokenService.setTokens).toHaveBeenCalledWith( accessToken, - refreshToken, mockVaultTimeoutAction, mockVaultTimeout, + refreshToken, ); expect(stateService.addAccount).toHaveBeenCalledWith( @@ -215,29 +225,6 @@ describe("LoginStrategy", () => { expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); - it("persists a device key for trusted device encryption when it exists on login", async () => { - // Arrange - const idTokenResponse = identityTokenResponseFactory(); - apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - - const deviceKey = new SymmetricCryptoKey( - new Uint8Array(userKeyBytesLength).buffer as CsprngArray, - ) as DeviceKey; - - stateService.getDeviceKey.mockResolvedValue(deviceKey); - - const accountKeys = new AccountKeys(); - accountKeys.deviceKey = deviceKey; - - // Act - await passwordLoginStrategy.logIn(credentials); - - // Assert - expect(stateService.addAccount).toHaveBeenCalledWith( - expect.objectContaining({ keys: accountKeys }), - ); - }); - it("builds AuthResult", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.forcePasswordReset = true; @@ -264,7 +251,7 @@ describe("LoginStrategy", () => { }); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); const result = await passwordLoginStrategy.logIn(credentials); @@ -283,7 +270,7 @@ describe("LoginStrategy", () => { cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); @@ -405,6 +392,8 @@ describe("LoginStrategy", () => { passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index eef5626493..a6dc193183 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,6 +1,8 @@ import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -26,11 +28,11 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { - AccountKeys, Account, AccountProfile, AccountTokens, } from "@bitwarden/common/platform/models/domain/account"; +import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { @@ -61,6 +63,8 @@ export abstract class LoginStrategy { protected abstract cache: BehaviorSubject; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -157,34 +161,21 @@ export abstract class LoginStrategy { * @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token. * @returns {Promise} - A promise that resolves when the account information has been successfully saved. */ - protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { + protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); - // Must persist existing device key if it exists for trusted device decryption to work - // However, we must provide a user id so that the device key can be retrieved - // as the state service won't have an active account at this point in time - // even though the data exists in local storage. const userId = accountInformation.sub; - const deviceKey = await this.stateService.getDeviceKey({ userId }); - const accountKeys = new AccountKeys(); - if (deviceKey) { - accountKeys.deviceKey = deviceKey; - } - - // If you don't persist existing admin auth requests on login, they will get deleted. - const adminAuthRequest = await this.stateService.getAdminAuthRequest({ userId }); - - const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); - const vaultTimeout = await this.stateService.getVaultTimeout(); + const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); + const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); // set access token and refresh token before account initialization so authN status can be accurate // User id will be derived from the access token. await this.tokenService.setTokens( tokenResponse.accessToken, - tokenResponse.refreshToken, vaultTimeoutAction as VaultTimeoutAction, vaultTimeout, + tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. ); await this.stateService.addAccount( @@ -204,8 +195,6 @@ export abstract class LoginStrategy { tokens: { ...new AccountTokens(), }, - keys: accountKeys, - adminAuthRequest: adminAuthRequest?.toJSON(), }), ); @@ -214,6 +203,7 @@ export abstract class LoginStrategy { ); await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); + return userId as UserId; } protected async processTokenResponse(response: IdentityTokenResponse): Promise { @@ -236,7 +226,7 @@ export abstract class LoginStrategy { } // Must come before setting keys, user key needs email to update additional keys - await this.saveAccountInformation(response); + const userId = await this.saveAccountInformation(response); if (response.twoFactorToken != null) { // note: we can read email from access token b/c it was saved in saveAccountInformation @@ -246,7 +236,7 @@ export abstract class LoginStrategy { } await this.setMasterKey(response); - await this.setUserKey(response); + await this.setUserKey(response, userId); await this.setPrivateKey(response); this.messagingService.send("loggedIn"); @@ -256,7 +246,7 @@ export abstract class LoginStrategy { // The keys comes from different sources depending on the login strategy protected abstract setMasterKey(response: IdentityTokenResponse): Promise; - protected abstract setUserKey(response: IdentityTokenResponse): Promise; + protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise; protected abstract setPrivateKey(response: IdentityTokenResponse): Promise; // Old accounts used master key for encryption. We are forcing migrations but only need to diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 470a4ac713..b902fff574 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -9,6 +9,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -19,11 +20,13 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction, PasswordStrengthService, } from "@bitwarden/common/tools/password-strength"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -42,6 +45,7 @@ const masterKey = new SymmetricCryptoKey( "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==", ), ) as MasterKey; +const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const masterPasswordPolicy = new MasterPasswordPolicyResponse({ EnforceOnLogin: true, @@ -50,6 +54,8 @@ const masterPasswordPolicy = new MasterPasswordPolicyResponse({ describe("PasswordLoginStrategy", () => { let cache: PasswordLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; let cryptoService: MockProxy; @@ -71,6 +77,9 @@ describe("PasswordLoginStrategy", () => { let tokenResponse: IdentityTokenResponse; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + loginStrategyService = mock(); cryptoService = mock(); apiService = mock(); @@ -102,6 +111,8 @@ describe("PasswordLoginStrategy", () => { passwordLoginStrategy = new PasswordLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -145,13 +156,16 @@ describe("PasswordLoginStrategy", () => { it("sets keys after a successful authentication", async () => { const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await passwordLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.setMasterKeyHash).toHaveBeenCalledWith(localHashedPassword); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith(masterKey, userId); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + localHashedPassword, + userId, + ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); @@ -183,8 +197,9 @@ describe("PasswordLoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); expect(policyService.evaluateMasterPassword).toHaveBeenCalled(); - expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, + userId, ); expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); @@ -222,8 +237,9 @@ describe("PasswordLoginStrategy", () => { expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None); // Second login attempt should save the force password reset options and return in result - expect(stateService.setForceSetPasswordReason).toHaveBeenCalledWith( + expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith( ForceSetPasswordReason.WeakMasterPassword, + userId, ); expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword); }); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index be93d39ebc..2490c35a00 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -1,9 +1,11 @@ -import { BehaviorSubject, map, Observable } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -23,6 +25,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { HashPurpose } from "@bitwarden/common/platform/enums"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; @@ -37,15 +40,11 @@ export class PasswordLoginStrategyData implements LoginStrategyData { /** User's entered email obtained pre-login. Always present in MP login. */ userEnteredEmail: string; - + /** If 2fa is required, token is returned to bypass captcha */ captchaBypassToken?: string; - /** - * The local version of the user's master key hash - */ + /** The local version of the user's master key hash */ localMasterKeyHash: string; - /** - * The user's master key - */ + /** The user's master key */ masterKey: MasterKey; /** * Tracks if the user needs to update their password due to @@ -63,19 +62,19 @@ export class PasswordLoginStrategyData implements LoginStrategyData { } export class PasswordLoginStrategy extends LoginStrategy { - /** - * The email address of the user attempting to log in. - */ + /** The email address of the user attempting to log in. */ email$: Observable; - /** - * The master key hash of the user attempting to log in. - */ - masterKeyHash$: Observable; + /** The master key hash used for authentication */ + serverMasterKeyHash$: Observable; + /** The local master key hash we store client side */ + localMasterKeyHash$: Observable; protected cache: BehaviorSubject; constructor( data: PasswordLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -92,6 +91,8 @@ export class PasswordLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -107,7 +108,10 @@ export class PasswordLoginStrategy extends LoginStrategy { this.cache = new BehaviorSubject(data); this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email)); - this.masterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash)); + this.serverMasterKeyHash$ = this.cache.pipe( + map((state) => state.tokenRequest.masterPasswordHash), + ); + this.localMasterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash)); } override async logIn(credentials: PasswordLoginCredentials) { @@ -123,11 +127,14 @@ export class PasswordLoginStrategy extends LoginStrategy { data.masterKey, HashPurpose.LocalAuthorization, ); - const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, data.masterKey); + const serverMasterKeyHash = await this.cryptoService.hashMasterKey( + masterPassword, + data.masterKey, + ); data.tokenRequest = new PasswordTokenRequest( email, - masterKeyHash, + serverMasterKeyHash, captchaToken, await this.buildTwoFactor(twoFactor, email), await this.buildDeviceRequest(), @@ -157,8 +164,10 @@ export class PasswordLoginStrategy extends LoginStrategy { }); } else { // Authentication was successful, save the force update password options with the state service - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.WeakMasterPassword, + userId, ); authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; } @@ -171,10 +180,10 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactor: TokenTwoFactorRequest, captchaResponse: string, ): Promise { - this.cache.next({ - ...this.cache.value, - captchaBypassToken: captchaResponse ?? this.cache.value.captchaBypassToken, - }); + const data = this.cache.value; + data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken; + this.cache.next(data); + const result = await super.logInTwoFactor(twoFactor); // 2FA was successful, save the force update password options with the state service if defined @@ -184,7 +193,8 @@ export class PasswordLoginStrategy extends LoginStrategy { !result.requiresCaptcha && forcePasswordResetReason != ForceSetPasswordReason.None ) { - await this.stateService.setForceSetPasswordReason(forcePasswordResetReason); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId); result.forcePasswordReset = forcePasswordResetReason; } @@ -193,18 +203,22 @@ export class PasswordLoginStrategy extends LoginStrategy { protected override async setMasterKey(response: IdentityTokenResponse) { const { masterKey, localMasterKeyHash } = this.cache.value; - await this.cryptoService.setMasterKey(masterKey); - await this.cryptoService.setMasterKeyHash(localMasterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); } - protected override async setUserKey(response: IdentityTokenResponse): Promise { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { // If migration is required, we won't have a user key to set yet. if (this.encryptionKeyMigrationRequired(response)) { return; } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - const masterKey = await this.cryptoService.getMasterKey(); + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index d4b0b13eaf..b78ad6dea6 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -9,6 +9,7 @@ import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/a import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -20,7 +21,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key"; import { @@ -33,6 +36,9 @@ import { identityTokenResponseFactory } from "./login.strategy.spec"; import { SsoLoginStrategy } from "./sso-login.strategy"; describe("SsoLoginStrategy", () => { + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + let cryptoService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -52,6 +58,7 @@ describe("SsoLoginStrategy", () => { let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; @@ -61,6 +68,9 @@ describe("SsoLoginStrategy", () => { const ssoOrgId = "SSO_ORG_ID"; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -83,6 +93,8 @@ describe("SsoLoginStrategy", () => { ssoLoginStrategy = new SsoLoginStrategy( null, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -130,7 +142,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); expect(cryptoService.setUserKey).not.toHaveBeenCalled(); expect(cryptoService.setPrivateKey).not.toHaveBeenCalled(); }); @@ -289,7 +301,7 @@ describe("SsoLoginStrategy", () => { id: "1", privateKey: "PRIVATE" as any, } as AdminAuthRequestStorable; - stateService.getAdminAuthRequest.mockResolvedValue( + authRequestService.getAdminAuthRequest.mockResolvedValue( new AdminAuthRequestStorable(adminAuthRequest), ); }); @@ -352,7 +364,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(stateService.setAdminAuthRequest).toHaveBeenCalledWith(null); + expect(authRequestService.clearAdminAuthRequest).toHaveBeenCalled(); expect( authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, ).not.toHaveBeenCalled(); @@ -395,7 +407,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -422,7 +434,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await ssoLoginStrategy.logIn(credentials); @@ -446,7 +458,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); await ssoLoginStrategy.logIn(credentials); @@ -473,7 +485,7 @@ describe("SsoLoginStrategy", () => { ) as MasterKey; apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await ssoLoginStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 04f158d30a..d8efd78984 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -1,9 +1,11 @@ -import { Observable, map, BehaviorSubject } from "rxjs"; +import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -20,6 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -78,6 +81,8 @@ export class SsoLoginStrategy extends LoginStrategy { constructor( data: SsoLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -95,6 +100,8 @@ export class SsoLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -137,7 +144,11 @@ export class SsoLoginStrategy extends LoginStrategy { // Auth guard currently handles redirects for this. if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { - await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ssoAuthResult.forcePasswordReset, + userId, + ); } this.cache.next({ @@ -205,7 +216,10 @@ export class SsoLoginStrategy extends LoginStrategy { // TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request) // so might be worth moving this logic to a common place (base login strategy or a separate service?) - protected override async setUserKey(tokenResponse: IdentityTokenResponse): Promise { + protected override async setUserKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise { const masterKeyEncryptedUserKey = tokenResponse.key; // Note: masterKeyEncryptedUserKey is undefined for SSO JIT provisioned users @@ -221,7 +235,7 @@ export class SsoLoginStrategy extends LoginStrategy { // Note: TDE and key connector are mutually exclusive if (userDecryptionOptions?.trustedDeviceOption) { - await this.trySetUserKeyWithApprovedAdminRequestIfExists(); + await this.trySetUserKeyWithApprovedAdminRequestIfExists(userId); const hasUserKey = await this.cryptoService.hasUserKey(); @@ -241,9 +255,9 @@ export class SsoLoginStrategy extends LoginStrategy { // is responsible for deriving master key from MP entry and then decrypting the user key } - private async trySetUserKeyWithApprovedAdminRequestIfExists(): Promise { + private async trySetUserKeyWithApprovedAdminRequestIfExists(userId: UserId): Promise { // At this point a user could have an admin auth request that has been approved - const adminAuthReqStorable = await this.stateService.getAdminAuthRequest(); + const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId); if (!adminAuthReqStorable) { return; @@ -257,7 +271,7 @@ export class SsoLoginStrategy extends LoginStrategy { } catch (error) { if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) { // if we get a 404, it means the auth request has been deleted so clear it from storage - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); } // Always return on an error here as we don't want to block the user from logging in @@ -284,11 +298,11 @@ export class SsoLoginStrategy extends LoginStrategy { if (await this.cryptoService.hasUserKey()) { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device - await this.deviceTrustCryptoService.trustDeviceIfRequired(); + await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); // if we successfully decrypted the user key, we can delete the admin auth request out of state // TODO: eventually we post and clean up DB as well once consumed on client - await this.stateService.setAdminAuthRequest(null); + await this.authRequestService.clearAdminAuthRequest(userId); this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved")); } @@ -298,7 +312,9 @@ export class SsoLoginStrategy extends LoginStrategy { private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise { const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption; - const deviceKey = await this.deviceTrustCryptoService.getDeviceKey(); + const userId = (await this.stateService.getUserId()) as UserId; + + const deviceKey = await this.deviceTrustCryptoService.getDeviceKey(userId); const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey; const encUserKey = trustedDeviceOption?.encryptedUserKey; @@ -307,6 +323,7 @@ export class SsoLoginStrategy extends LoginStrategy { } const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + userId, encDevicePrivateKey, encUserKey, deviceKey, @@ -318,7 +335,8 @@ export class SsoLoginStrategy extends LoginStrategy { } private async trySetUserKeyWithMasterKey(): Promise { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); // There is a scenario in which the master key is not set here. That will occur if the user // has a master password and is using Key Connector. In that case, we cannot set the master key diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 02aed305a4..5e7d7985b1 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -19,7 +20,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -30,6 +33,8 @@ import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login describe("UserApiLoginStrategy", () => { let cache: UserApiLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy; let apiService: MockProxy; @@ -48,12 +53,16 @@ describe("UserApiLoginStrategy", () => { let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; + const userId = Utils.newGuid() as UserId; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; const apiClientId = "API_CLIENT_ID"; const apiClientSecret = "API_CLIENT_SECRET"; beforeEach(async () => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -74,6 +83,8 @@ describe("UserApiLoginStrategy", () => { apiLogInStrategy = new UserApiLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -172,7 +183,7 @@ describe("UserApiLoginStrategy", () => { environmentService.environment$ = new BehaviorSubject(env); apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.getMasterKey.mockResolvedValue(masterKey); + masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await apiLogInStrategy.logIn(credentials); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 2af666f95c..4a0d005b1c 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -2,7 +2,9 @@ import { firstValueFrom, BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request"; @@ -16,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; @@ -39,6 +42,8 @@ export class UserApiLoginStrategy extends LoginStrategy { constructor( data: UserApiLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +59,8 @@ export class UserApiLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -91,11 +98,15 @@ export class UserApiLoginStrategy extends LoginStrategy { } } - protected override async setUserKey(response: IdentityTokenResponse): Promise { + protected override async setUserKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); if (response.apiUseKeyConnector) { - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); await this.cryptoService.setUserKey(userKey); @@ -109,8 +120,8 @@ export class UserApiLoginStrategy extends LoginStrategy { ); } - protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) { - await super.saveAccountInformation(tokenResponse); + protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { + const userId = await super.saveAccountInformation(tokenResponse); const vaultTimeout = await this.stateService.getVaultTimeout(); const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction(); @@ -127,6 +138,7 @@ export class UserApiLoginStrategy extends LoginStrategy { vaultTimeoutAction as VaultTimeoutAction, vaultTimeout, ); + return userId; } exportCache(): CacheData { diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index edc1441361..1d96921286 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -6,6 +6,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; @@ -16,6 +17,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { FakeAccountService } from "@bitwarden/common/spec"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -26,6 +28,8 @@ import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-log describe("WebAuthnLoginStrategy", () => { let cache: WebAuthnLoginStrategyData; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService!: MockProxy; let apiService!: MockProxy; @@ -63,6 +67,9 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); + accountService = new FakeAccountService(null); + masterPasswordService = new FakeMasterPasswordService(); + cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -81,6 +88,8 @@ describe("WebAuthnLoginStrategy", () => { webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -207,7 +216,7 @@ describe("WebAuthnLoginStrategy", () => { expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey); // Master key and private key should not be set - expect(cryptoService.setMasterKey).not.toHaveBeenCalled(); + expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); }); it("does not try to set the user key when prfKey is missing", async () => { diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index a8e67597b8..8a62a8fb3c 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -2,6 +2,8 @@ import { BehaviorSubject } from "rxjs"; import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -15,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions"; @@ -41,6 +44,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { constructor( data: WebAuthnLoginStrategyData, + accountService: AccountService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, cryptoService: CryptoService, apiService: ApiService, tokenService: TokenService, @@ -54,6 +59,8 @@ export class WebAuthnLoginStrategy extends LoginStrategy { billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, @@ -92,7 +99,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { return Promise.resolve(); } - protected override async setUserKey(idTokenResponse: IdentityTokenResponse) { + protected override async setUserKey(idTokenResponse: IdentityTokenResponse, userId: UserId) { const masterKeyEncryptedUserKey = idTokenResponse.key; if (masterKeyEncryptedUserKey) { diff --git a/libs/auth/src/common/models/domain/user-decryption-options.ts b/libs/auth/src/common/models/domain/user-decryption-options.ts index c600c8be47..ca4046f36e 100644 --- a/libs/auth/src/common/models/domain/user-decryption-options.ts +++ b/libs/auth/src/common/models/domain/user-decryption-options.ts @@ -15,11 +15,14 @@ export class KeyConnectorUserDecryptionOption { /** * Initializes a new instance of the KeyConnectorUserDecryptionOption from a response object. * @param response The key connector user decryption option response object. - * @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the response is nullish. + * @returns A new instance of the KeyConnectorUserDecryptionOption or undefined if `response` is nullish. */ static fromResponse( response: KeyConnectorUserDecryptionOptionResponse, - ): KeyConnectorUserDecryptionOption { + ): KeyConnectorUserDecryptionOption | undefined { + if (response == null) { + return undefined; + } const options = new KeyConnectorUserDecryptionOption(); options.keyConnectorUrl = response?.keyConnectorUrl ?? null; return options; @@ -28,11 +31,14 @@ export class KeyConnectorUserDecryptionOption { /** * Initializes a new instance of a KeyConnectorUserDecryptionOption from a JSON object. * @param obj JSON object to deserialize. - * @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the JSON object is nullish. + * @returns A new instance of the KeyConnectorUserDecryptionOption or undefined if `obj` is nullish. */ static fromJSON( obj: Jsonify, - ): KeyConnectorUserDecryptionOption { + ): KeyConnectorUserDecryptionOption | undefined { + if (obj == null) { + return undefined; + } return Object.assign(new KeyConnectorUserDecryptionOption(), obj); } } @@ -52,11 +58,14 @@ export class TrustedDeviceUserDecryptionOption { /** * Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object. * @param response The trusted device user decryption option response object. - * @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the response is nullish. + * @returns A new instance of the TrustedDeviceUserDecryptionOption or undefined if `response` is nullish. */ static fromResponse( response: TrustedDeviceUserDecryptionOptionResponse, - ): TrustedDeviceUserDecryptionOption { + ): TrustedDeviceUserDecryptionOption | undefined { + if (response == null) { + return undefined; + } const options = new TrustedDeviceUserDecryptionOption(); options.hasAdminApproval = response?.hasAdminApproval ?? false; options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false; @@ -67,11 +76,14 @@ export class TrustedDeviceUserDecryptionOption { /** * Initializes a new instance of the TrustedDeviceUserDecryptionOption from a JSON object. * @param obj JSON object to deserialize. - * @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the JSON object is nullish. + * @returns A new instance of the TrustedDeviceUserDecryptionOption or undefined if `obj` is nullish. */ static fromJSON( obj: Jsonify, - ): TrustedDeviceUserDecryptionOption { + ): TrustedDeviceUserDecryptionOption | undefined { + if (obj == null) { + return undefined; + } return Object.assign(new TrustedDeviceUserDecryptionOption(), obj); } } diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index b1971f6b52..5907048684 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -2,12 +2,16 @@ import { mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; +import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { AuthRequestService } from "./auth-request.service"; @@ -15,21 +19,74 @@ import { AuthRequestService } from "./auth-request.service"; describe("AuthRequestService", () => { let sut: AuthRequestService; + const stateProvider = mock(); + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; const appIdService = mock(); const cryptoService = mock(); const apiService = mock(); - const stateService = mock(); let mockPrivateKey: Uint8Array; + const mockUserId = Utils.newGuid() as UserId; beforeEach(() => { jest.clearAllMocks(); + accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); - sut = new AuthRequestService(appIdService, cryptoService, apiService, stateService); + sut = new AuthRequestService( + appIdService, + accountService, + masterPasswordService, + cryptoService, + apiService, + stateProvider, + ); mockPrivateKey = new Uint8Array(64); }); + describe("authRequestPushNotification$", () => { + it("should emit when sendAuthRequestPushNotification is called", () => { + const notification = { + id: "PUSH_NOTIFICATION", + userId: "USER_ID", + } as AuthRequestPushNotification; + + const spy = jest.fn(); + sut.authRequestPushNotification$.subscribe(spy); + + sut.sendAuthRequestPushNotification(notification); + + expect(spy).toHaveBeenCalledWith("PUSH_NOTIFICATION"); + }); + }); + + describe("AcceptAuthRequests", () => { + it("returns an error when userId isn't provided", async () => { + await expect(sut.getAcceptAuthRequests(undefined)).rejects.toThrow("User ID is required"); + await expect(sut.setAcceptAuthRequests(true, undefined)).rejects.toThrow( + "User ID is required", + ); + }); + }); + + describe("AdminAuthRequest", () => { + it("returns an error when userId isn't provided", async () => { + await expect(sut.getAdminAuthRequest(undefined)).rejects.toThrow("User ID is required"); + await expect(sut.setAdminAuthRequest(undefined, undefined)).rejects.toThrow( + "User ID is required", + ); + await expect(sut.clearAdminAuthRequest(undefined)).rejects.toThrow("User ID is required"); + }); + + it("does not allow clearing from setAdminAuthRequest", async () => { + await expect(sut.setAdminAuthRequest(null, "USER_ID" as UserId)).rejects.toThrow( + "Auth request is required", + ); + }); + }); + describe("approveOrDenyAuthRequest", () => { beforeEach(() => { cryptoService.rsaEncrypt.mockResolvedValue({ @@ -50,8 +107,8 @@ describe("AuthRequestService", () => { }); it("should use the master key and hash if they exist", async () => { - cryptoService.getMasterKey.mockResolvedValueOnce({ encKey: new Uint8Array(64) } as MasterKey); - stateService.getKeyHash.mockResolvedValueOnce("KEY_HASH"); + masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey); + masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH"); await sut.approveOrDenyAuthRequest( true, @@ -113,8 +170,8 @@ describe("AuthRequestService", () => { masterKeyHash: mockDecryptedMasterKeyHash, }); - cryptoService.setMasterKey.mockResolvedValueOnce(undefined); - cryptoService.setMasterKeyHash.mockResolvedValueOnce(undefined); + masterPasswordService.masterKeySubject.next(undefined); + masterPasswordService.masterKeyHashSubject.next(undefined); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey); cryptoService.setUserKey.mockResolvedValueOnce(undefined); @@ -127,10 +184,18 @@ describe("AuthRequestService", () => { mockAuthReqResponse.masterPasswordHash, mockPrivateKey, ); - expect(cryptoService.setMasterKey).toBeCalledWith(mockDecryptedMasterKey); - expect(cryptoService.setMasterKeyHash).toBeCalledWith(mockDecryptedMasterKeyHash); - expect(cryptoService.decryptUserKeyWithMasterKey).toBeCalledWith(mockDecryptedMasterKey); - expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + mockDecryptedMasterKey, + mockUserId, + ); + expect(masterPasswordService.mock.setMasterKeyHash).toHaveBeenCalledWith( + mockDecryptedMasterKeyHash, + mockUserId, + ); + expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( + mockDecryptedMasterKey, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey); }); }); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index ab33780fe6..062a10af14 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -1,22 +1,118 @@ +import { Observable, Subject, firstValueFrom } from "rxjs"; +import { Jsonify } from "type-fest"; + import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; +import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { + AUTH_REQUEST_DISK_LOCAL, + StateProvider, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; +/** + * Disk-local to maintain consistency between tabs (even though + * approvals are currently only available on desktop). We don't + * want to clear this on logout as it's a user preference. + */ +export const ACCEPT_AUTH_REQUESTS_KEY = new UserKeyDefinition( + AUTH_REQUEST_DISK_LOCAL, + "acceptAuthRequests", + { + deserializer: (value) => value ?? false, + clearOn: [], + }, +); + +/** + * Disk-local to maintain consistency between tabs. We don't want to + * clear this on logout since admin auth requests are long-lived. + */ +export const ADMIN_AUTH_REQUEST_KEY = new UserKeyDefinition>( + AUTH_REQUEST_DISK_LOCAL, + "adminAuthRequest", + { + deserializer: (value) => value, + clearOn: [], + }, +); + export class AuthRequestService implements AuthRequestServiceAbstraction { + private authRequestPushNotificationSubject = new Subject(); + authRequestPushNotification$: Observable; + constructor( private appIdService: AppIdService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, - private stateService: StateService, - ) {} + private stateProvider: StateProvider, + ) { + this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable(); + } + + async getAcceptAuthRequests(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + const value = await firstValueFrom( + this.stateProvider.getUser(userId, ACCEPT_AUTH_REQUESTS_KEY).state$, + ); + return value; + } + + async setAcceptAuthRequests(accept: boolean, userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + await this.stateProvider.setUserState(ACCEPT_AUTH_REQUESTS_KEY, accept, userId); + } + + async getAdminAuthRequest(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + const authRequestSerialized = await firstValueFrom( + this.stateProvider.getUser(userId, ADMIN_AUTH_REQUEST_KEY).state$, + ); + const adminAuthRequestStorable = AdminAuthRequestStorable.fromJSON(authRequestSerialized); + return adminAuthRequestStorable; + } + + async setAdminAuthRequest(authRequest: AdminAuthRequestStorable, userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + if (authRequest == null) { + throw new Error("Auth request is required"); + } + + await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, authRequest.toJSON(), userId); + } + + async clearAdminAuthRequest(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required"); + } + + await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId); + } async approveOrDenyAuthRequest( approve: boolean, @@ -30,8 +126,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { } const pubKey = Utils.fromB64ToArray(authRequest.publicKey); - const masterKey = await this.cryptoService.getMasterKey(); - const masterKeyHash = await this.stateService.getKeyHash(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); let encryptedMasterKeyHash; let keyToEncrypt; @@ -84,8 +181,9 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); // Set masterKey + masterKeyHash in state after decryption (in case decryption fails) - await this.cryptoService.setMasterKey(masterKey); - await this.cryptoService.setMasterKeyHash(masterKeyHash); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + await this.masterPasswordService.setMasterKey(masterKey, userId); + await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); await this.cryptoService.setUserKey(userKey); } @@ -126,4 +224,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { masterKeyHash, }; } + + sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void { + if (notification.id != null) { + this.authRequestPushNotificationSubject.next(notification.id); + } + } } diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index 12215cf6b4..5a0fc083dd 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -1,4 +1,5 @@ export * from "./pin-crypto/pin-crypto.service.implementation"; +export * from "./login-email/login-email.service"; export * from "./login-strategies/login-strategy.service"; export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts new file mode 100644 index 0000000000..171af07430 --- /dev/null +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -0,0 +1,52 @@ +import { Observable } from "rxjs"; + +import { + GlobalState, + KeyDefinition, + LOGIN_EMAIL_DISK, + StateProvider, +} from "../../../../../common/src/platform/state"; +import { LoginEmailServiceAbstraction } from "../../abstractions/login-email.service"; + +const STORED_EMAIL = new KeyDefinition(LOGIN_EMAIL_DISK, "storedEmail", { + deserializer: (value: string) => value, +}); + +export class LoginEmailService implements LoginEmailServiceAbstraction { + private email: string; + private rememberEmail: boolean; + + private readonly storedEmailState: GlobalState; + storedEmail$: Observable; + + constructor(private stateProvider: StateProvider) { + this.storedEmailState = this.stateProvider.getGlobal(STORED_EMAIL); + this.storedEmail$ = this.storedEmailState.state$; + } + + getEmail() { + return this.email; + } + + setEmail(email: string) { + this.email = email; + } + + getRememberEmail() { + return this.rememberEmail; + } + + setRememberEmail(value: boolean) { + this.rememberEmail = value; + } + + clearValues() { + this.email = null; + this.rememberEmail = null; + } + + async saveEmailSettings() { + await this.storedEmailState.update(() => (this.rememberEmail ? this.email : null)); + this.clearValues(); + } +} diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 981e4d81ac..fcc0220d0a 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -11,6 +11,7 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response"; +import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,8 +23,14 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; -import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; +import { + FakeAccountService, + FakeGlobalState, + FakeGlobalStateProvider, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { AuthRequestServiceAbstraction, @@ -38,6 +45,8 @@ import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; describe("LoginStrategyService", () => { let sut: LoginStrategyService; + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cryptoService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -61,7 +70,11 @@ describe("LoginStrategyService", () => { let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; + const userId = "USER_ID" as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock(); apiService = mock(); tokenService = mock(); @@ -84,6 +97,8 @@ describe("LoginStrategyService", () => { stateProvider = new FakeGlobalStateProvider(); sut = new LoginStrategyService( + accountService, + masterPasswordService, cryptoService, apiService, tokenService, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 5dbc3397cf..a8bd7bc2ff 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -1,7 +1,6 @@ import { combineLatestWith, distinctUntilChanged, - filter, firstValueFrom, map, Observable, @@ -10,8 +9,10 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; @@ -23,7 +24,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -81,10 +81,10 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { >; currentAuthType$: Observable; - // TODO: move to auth request service - authRequestPushNotification$: Observable; constructor( + protected accountService: AccountService, + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected cryptoService: CryptoService, protected apiService: ApiService, protected tokenService: TokenService, @@ -114,9 +114,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ); this.currentAuthType$ = this.currentAuthnTypeState.state$; - this.authRequestPushNotification$ = this.authRequestPushNotificationState.state$.pipe( - filter((id) => id != null), - ); this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe( distinctUntilChanged(), combineLatestWith(this.loginStrategyCacheState.state$), @@ -137,8 +134,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async getMasterPasswordHash(): Promise { const strategy = await firstValueFrom(this.loginStrategy$); - if ("masterKeyHash$" in strategy) { - return await firstValueFrom(strategy.masterKeyHash$); + if ("serverMasterKeyHash$" in strategy) { + return await firstValueFrom(strategy.serverMasterKeyHash$); } return null; } @@ -256,13 +253,6 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); } - // TODO move to auth request service - async sendAuthRequestPushNotification(notification: AuthRequestPushNotification): Promise { - if (notification.id != null) { - await this.authRequestPushNotificationState.update((_) => notification.id); - } - } - // TODO: move to auth request service async passwordlessLogin( id: string, @@ -271,7 +261,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { ): Promise { const pubKey = Utils.fromB64ToArray(key); - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); let keyToEncrypt; let encryptedMasterKeyHash = null; @@ -280,7 +271,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { // Only encrypt the master password hash if masterKey exists as // we won't have a masterKeyHash without a masterKey - const masterKeyHash = await this.stateService.getKeyHash(); + const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId)); if (masterKeyHash != null) { encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt( Utils.fromUtf8ToArray(masterKeyHash), @@ -347,6 +338,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Password: return new PasswordLoginStrategy( data?.password, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -365,6 +358,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.Sso: return new SsoLoginStrategy( data?.sso, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -384,6 +379,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( data?.userApiKey, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -401,6 +398,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( data?.authRequest, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, @@ -417,6 +416,8 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { case AuthenticationType.WebAuthn: return new WebAuthnLoginStrategy( data?.webAuthn, + this.accountService, + this.masterPasswordService, this.cryptoService, this.apiService, this.tokenService, diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts index e8bb1b38ce..16479f19ea 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts @@ -1,6 +1,5 @@ import { firstValueFrom } from "rxjs"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FakeAccountService, @@ -66,7 +65,6 @@ describe("UserDecryptionOptionsService", () => { await fakeAccountService.addAccount(givenUser, { name: "Test User 1", email: "test1@email.com", - status: AuthenticationStatus.Locked, }); await fakeStateProvider.setUserState( USER_DECRYPTION_OPTIONS, diff --git a/libs/auth/src/icons/bitwarden-logo.ts b/libs/auth/src/icons/bitwarden-logo.ts index 90591e0fe7..872228e75d 100644 --- a/libs/auth/src/icons/bitwarden-logo.ts +++ b/libs/auth/src/icons/bitwarden-logo.ts @@ -3,7 +3,7 @@ import { svgIcon } from "@bitwarden/components"; export const BitwardenLogo = svgIcon` Bitwarden - - + + `; diff --git a/libs/auth/src/icons/icon-lock.ts b/libs/auth/src/icons/icon-lock.ts index b56c1ea36d..61330fe0df 100644 --- a/libs/auth/src/icons/icon-lock.ts +++ b/libs/auth/src/icons/icon-lock.ts @@ -2,6 +2,6 @@ import { svgIcon } from "@bitwarden/components"; export const IconLock = svgIcon` - + `; diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index 2f33d9cf02..a8b09b7417 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -1,8 +1,7 @@ import { mock } from "jest-mock-extended"; -import { Observable, ReplaySubject } from "rxjs"; +import { ReplaySubject } from "rxjs"; import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; -import { AuthenticationStatus } from "../src/auth/enums/authentication-status"; import { UserId } from "../src/types/guid"; export function mockAccountServiceWith( @@ -14,7 +13,6 @@ export function mockAccountServiceWith( ...{ name: "name", email: "email", - status: AuthenticationStatus.Locked, }, }; const service = new FakeAccountService({ [userId]: fullInfo }); @@ -32,14 +30,8 @@ export class FakeAccountService implements AccountService { get activeUserId() { return this._activeUserId; } - get accounts$() { - return this.accountsSubject.asObservable(); - } - get activeAccount$() { - return this.activeAccountSubject.asObservable(); - } - accountLock$: Observable; - accountLogout$: Observable; + accounts$ = this.accountsSubject.asObservable(); + activeAccount$ = this.activeAccountSubject.asObservable(); constructor(initialData: Record) { this.accountsSubject.next(initialData); @@ -61,14 +53,6 @@ export class FakeAccountService implements AccountService { await this.mock.setAccountEmail(userId, email); } - async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise { - await this.mock.setAccountStatus(userId, status); - } - - async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise { - await this.mock.setMaxAccountStatus(userId, maxStatus); - } - async switchAccount(userId: UserId): Promise { const next = userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] }; diff --git a/libs/common/spec/matchers/to-almost-equal.spec.ts b/libs/common/spec/matchers/to-almost-equal.spec.ts new file mode 100644 index 0000000000..5922545137 --- /dev/null +++ b/libs/common/spec/matchers/to-almost-equal.spec.ts @@ -0,0 +1,54 @@ +describe("toAlmostEqual custom matcher", () => { + it("matches identical Dates", () => { + const date = new Date(); + expect(date).toAlmostEqual(date); + }); + + it("matches when older but within default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 5); + expect(date).toAlmostEqual(olderDate); + }); + + it("matches when newer but within default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 5); + expect(date).toAlmostEqual(olderDate); + }); + + it("doesn't match if older than default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 11); + expect(date).not.toAlmostEqual(olderDate); + }); + + it("doesn't match if newer than default ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 11); + expect(date).not.toAlmostEqual(olderDate); + }); + + it("matches when older but within custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 15); + expect(date).toAlmostEqual(olderDate, 20); + }); + + it("matches when newer but within custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 15); + expect(date).toAlmostEqual(olderDate, 20); + }); + + it("doesn't match if older than custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() - 21); + expect(date).not.toAlmostEqual(olderDate, 20); + }); + + it("doesn't match if newer than custom ms", () => { + const date = new Date(); + const olderDate = new Date(date.getTime() + 21); + expect(date).not.toAlmostEqual(olderDate, 20); + }); +}); diff --git a/libs/common/spec/matchers/to-almost-equal.ts b/libs/common/spec/matchers/to-almost-equal.ts new file mode 100644 index 0000000000..ba5aacc9b3 --- /dev/null +++ b/libs/common/spec/matchers/to-almost-equal.ts @@ -0,0 +1,20 @@ +/** + * Matches the expected date within an optional ms precision + * @param received The received date + * @param expected The expected date + * @param msPrecision The optional precision in milliseconds + */ +export const toAlmostEqual: jest.CustomMatcher = function ( + received: Date, + expected: Date, + msPrecision: number = 10, +) { + const receivedTime = received.getTime(); + const expectedTime = expected.getTime(); + const difference = Math.abs(receivedTime - expectedTime); + return { + pass: difference <= msPrecision, + message: () => + `expected ${received} to be within ${msPrecision}ms of ${expected} (actual difference: ${difference}ms)`, + }; +}; diff --git a/libs/common/spec/observable-tracker.ts b/libs/common/spec/observable-tracker.ts new file mode 100644 index 0000000000..a6f3e6a879 --- /dev/null +++ b/libs/common/spec/observable-tracker.ts @@ -0,0 +1,86 @@ +import { Observable, Subscription, firstValueFrom, throwError, timeout } from "rxjs"; + +/** Test class to enable async awaiting of observable emissions */ +export class ObservableTracker { + private subscription: Subscription; + emissions: T[] = []; + constructor(private observable: Observable) { + this.emissions = this.trackEmissions(observable); + } + + /** Unsubscribes from the observable */ + unsubscribe() { + this.subscription.unsubscribe(); + } + + /** + * Awaits the next emission from the observable, or throws if the timeout is exceeded + * @param msTimeout The maximum time to wait for another emission before throwing + */ + async expectEmission(msTimeout = 50) { + await firstValueFrom( + this.observable.pipe( + timeout({ + first: msTimeout, + with: () => throwError(() => new Error("Timeout exceeded waiting for another emission.")), + }), + ), + ); + } + + /** Awaits until the the total number of emissions observed by this tracker equals or exceeds {@link count} + * @param count The number of emissions to wait for + */ + async pauseUntilReceived(count: number, msTimeout = 50): Promise { + for (let i = 0; i < count - this.emissions.length; i++) { + await this.expectEmission(msTimeout); + } + return this.emissions; + } + + private trackEmissions(observable: Observable): T[] { + const emissions: T[] = []; + this.subscription = observable.subscribe((value) => { + switch (value) { + case undefined: + case null: + emissions.push(value); + return; + default: + // process by type + break; + } + + switch (typeof value) { + case "string": + case "number": + case "boolean": + emissions.push(value); + break; + case "symbol": + // Cheating types to make symbols work at all + emissions.push(value.toString() as T); + break; + default: { + emissions.push(clone(value)); + } + } + }); + return emissions; + } +} +function clone(value: any): any { + if (global.structuredClone != undefined) { + return structuredClone(value); + } else { + return JSON.parse(JSON.stringify(value)); + } +} + +/** A test helper that builds an @see{@link ObservableTracker}, which can be used to assert things about the + * emissions of the given observable + * @param observable The observable to track + */ +export function subscribeTo(observable: Observable) { + return new ObservableTracker(observable); +} diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 20ed3216a5..9b3160ee19 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -4,8 +4,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; -import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request"; -import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; @@ -29,7 +27,6 @@ import { ProviderUserResponse, ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; -import { ProviderResponse } from "../admin-console/models/response/provider/provider.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request"; @@ -220,7 +217,7 @@ export abstract class ApiService { putMoveCiphers: (request: CipherBulkMoveRequest) => Promise; putShareCipher: (id: string, request: CipherShareRequest) => Promise; putShareCiphers: (request: CipherBulkShareRequest) => Promise; - putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; + putCipherCollections: (id: string, request: CipherCollectionsRequest) => Promise; putCipherCollectionsAdmin: (id: string, request: CipherCollectionsRequest) => Promise; postPurgeCiphers: (request: SecretVerificationRequest, organizationId?: string) => Promise; putDeleteCipher: (id: string) => Promise; @@ -297,7 +294,6 @@ export abstract class ApiService { ) => Promise; getGroupUsers: (organizationId: string, id: string) => Promise; - putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise; deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise; getSync: () => Promise; @@ -373,10 +369,6 @@ export abstract class ApiService { getPlans: () => Promise>; getTaxRates: () => Promise>; - postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; - getProvider: (id: string) => Promise; - putProvider: (id: string, request: ProviderUpdateRequest) => Promise; - getProviderUsers: (providerId: string) => Promise>; getProviderUser: (providerId: string, id: string) => Promise; postProviderUserInvite: (providerId: string, request: ProviderUserInviteRequest) => Promise; diff --git a/libs/common/src/abstractions/search.service.ts b/libs/common/src/abstractions/search.service.ts index 97a12c8315..dfcf2c5d07 100644 --- a/libs/common/src/abstractions/search.service.ts +++ b/libs/common/src/abstractions/search.service.ts @@ -1,11 +1,15 @@ +import { Observable } from "rxjs"; + import { SendView } from "../tools/send/models/view/send.view"; +import { IndexedEntityId } from "../types/guid"; import { CipherView } from "../vault/models/view/cipher.view"; export abstract class SearchService { - indexedEntityId?: string = null; - clearIndex: () => void; - isSearchable: (query: string) => boolean; - indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => void; + indexedEntityId$: Observable; + + clearIndex: () => Promise; + isSearchable: (query: string) => Promise; + indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise; searchCiphers: ( query: string, filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[], diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index db064b2fd3..b4d7b1ffb2 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -3,9 +3,9 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; +import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingResponse } from "../../../billing/models/response/billing.response"; @@ -68,7 +68,7 @@ export class OrganizationApiServiceAbstraction { ) => Promise>; rotateApiKey: (id: string, request: OrganizationApiKeyRequest) => Promise; getTaxInfo: (id: string) => Promise; - updateTaxInfo: (id: string, request: OrganizationTaxInfoUpdateRequest) => Promise; + updateTaxInfo: (id: string, request: ExpandedTaxInfoUpdateRequest) => Promise; getKeys: (id: string) => Promise; updateKeys: (id: string, request: OrganizationKeysRequest) => Promise; getSso: (id: string) => Promise; diff --git a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts index 9cc4bba0eb..fefcac3a57 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization.service.abstraction.ts @@ -116,12 +116,16 @@ export abstract class OrganizationService { * https://bitwarden.atlassian.net/browse/AC-2252. */ getFromState: (id: string) => Promise; - canManageSponsorships: () => Promise; + canManageSponsorships$: Observable; hasOrganizations: () => Promise; get$: (id: string) => Observable; get: (id: string) => Promise; getAll: (userId?: string) => Promise; - // + + /** + * Publishes state for all organizations for the given user id or the active user. + */ + getAll$: (userId?: UserId) => Observable; } /** diff --git a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts index fb805f94cd..21669f78ad 100644 --- a/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/policy/policy.service.abstraction.ts @@ -78,5 +78,4 @@ export abstract class PolicyService { export abstract class InternalPolicyService extends PolicyService { upsert: (policy: PolicyData) => Promise; replace: (policies: { [id: string]: PolicyData }) => Promise; - clear: (userId?: string) => Promise; } diff --git a/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts new file mode 100644 index 0000000000..3c2170bf9e --- /dev/null +++ b/libs/common/src/admin-console/abstractions/provider/provider-api.service.abstraction.ts @@ -0,0 +1,15 @@ +import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; +import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; +import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { ProviderResponse } from "../../models/response/provider/provider.response"; + +export class ProviderApiServiceAbstraction { + postProviderSetup: (id: string, request: ProviderSetupRequest) => Promise; + getProvider: (id: string) => Promise; + putProvider: (id: string, request: ProviderUpdateRequest) => Promise; + providerRecoverDeleteToken: ( + organizationId: string, + request: ProviderVerifyRecoverDeleteRequest, + ) => Promise; + deleteProvider: (id: string) => Promise; +} diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index 18b762207a..bdf0b8fbbf 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -188,18 +188,30 @@ export class Organization { return this.isManager || this.permissions.createNewCollections; } - get canEditAnyCollection() { + canEditAnyCollection(flexibleCollectionsV1Enabled: boolean) { + if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) { + // Pre-Flexible Collections v1 logic + return this.isAdmin || this.permissions.editAnyCollection; + } + + // Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins + // Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag + return ( + this.isProviderUser || + (this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) || + (this.allowAdminAccessToAllCollectionItems && this.isAdmin) + ); + } + + canEditUnassignedCiphers() { + // TODO: Update this to exclude Providers if provider access is restricted in AC-1707 return this.isAdmin || this.permissions.editAnyCollection; } - get canUseAdminCollections() { - return this.canEditAnyCollection; - } - canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) { - // Before Flexible Collections, anyone with editAnyCollection permission could edit all ciphers - if (!flexibleCollectionsV1Enabled) { - return this.canEditAnyCollection; + // Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers + if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) { + return this.isAdmin || this.permissions.editAnyCollection; } // Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins // Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag @@ -214,8 +226,13 @@ export class Organization { return this.isAdmin || this.permissions.deleteAnyCollection; } + /** + * Whether the user can view all collection information, such as collection name and access. + * This does not indicate that the user can view items inside any collection - for that, see {@link canEditAllCiphers} + */ get canViewAllCollections() { - return this.canEditAnyCollection || this.canDeleteAnyCollection; + // Admins can always see all collections even if collection management settings prevent them from editing them or seeing items + return this.isAdmin || this.permissions.editAnyCollection || this.canDeleteAnyCollection; } /** diff --git a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts index 61eb943f1d..7dc664869c 100644 --- a/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts +++ b/libs/common/src/admin-console/models/request/provider/provider-setup.request.ts @@ -1,7 +1,10 @@ +import { ExpandedTaxInfoUpdateRequest } from "../../../../billing/models/request/expanded-tax-info-update.request"; + export class ProviderSetupRequest { name: string; businessName: string; billingEmail: string; token: string; key: string; + taxInfo: ExpandedTaxInfoUpdateRequest; } diff --git a/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts b/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts new file mode 100644 index 0000000000..528d2dba78 --- /dev/null +++ b/libs/common/src/admin-console/models/request/provider/provider-verify-recover-delete.request.ts @@ -0,0 +1,7 @@ +export class ProviderVerifyRecoverDeleteRequest { + token: string; + + constructor(token: string) { + this.token = token; + } +} diff --git a/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts b/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts index fb515e3cbc..c316a1d27c 100644 --- a/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts +++ b/libs/common/src/admin-console/services/organization-domain/requests/organization-domain.request.ts @@ -1,9 +1,7 @@ export class OrganizationDomainRequest { - txt: string; domainName: string; - constructor(txt: string, domainName: string) { - this.txt = txt; + constructor(domainName: string) { this.domainName = domainName; } } diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index af909a84f6..9765dd0e99 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -4,9 +4,9 @@ import { OrganizationSsoRequest } from "../../../auth/models/request/organizatio import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; import { ApiKeyResponse } from "../../../auth/models/response/api-key.response"; import { OrganizationSsoResponse } from "../../../auth/models/response/organization-sso.response"; +import { ExpandedTaxInfoUpdateRequest } from "../../../billing/models/request/expanded-tax-info-update.request"; import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models/request/organization-sm-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; -import { OrganizationTaxInfoUpdateRequest } from "../../../billing/models/request/organization-tax-info-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; import { BillingResponse } from "../../../billing/models/response/billing.response"; @@ -271,7 +271,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction return new TaxInfoResponse(r); } - async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise { + async updateTaxInfo(id: string, request: ExpandedTaxInfoUpdateRequest): Promise { // Can't broadcast anything because the response doesn't have content return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false); } diff --git a/libs/common/src/admin-console/services/organization/organization.service.spec.ts b/libs/common/src/admin-console/services/organization/organization.service.spec.ts index 908f4b8e28..6d2525966b 100644 --- a/libs/common/src/admin-console/services/organization/organization.service.spec.ts +++ b/libs/common/src/admin-console/services/organization/organization.service.spec.ts @@ -121,7 +121,7 @@ describe("OrganizationService", () => { const mockData: OrganizationData[] = buildMockOrganizations(1); mockData[0].familySponsorshipAvailable = true; fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await organizationService.canManageSponsorships(); + const result = await firstValueFrom(organizationService.canManageSponsorships$); expect(result).toBe(true); }); @@ -129,7 +129,7 @@ describe("OrganizationService", () => { const mockData: OrganizationData[] = buildMockOrganizations(1); mockData[0].familySponsorshipFriendlyName = "Something"; fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await organizationService.canManageSponsorships(); + const result = await firstValueFrom(organizationService.canManageSponsorships$); expect(result).toBe(true); }); @@ -137,7 +137,7 @@ describe("OrganizationService", () => { const mockData: OrganizationData[] = buildMockOrganizations(1); mockData[0].familySponsorshipFriendlyName = null; fakeActiveUserState.nextState(arrayToRecord(mockData)); - const result = await organizationService.canManageSponsorships(); + const result = await firstValueFrom(organizationService.canManageSponsorships$); expect(result).toBe(false); }); }); diff --git a/libs/common/src/admin-console/services/organization/organization.service.ts b/libs/common/src/admin-console/services/organization/organization.service.ts index 3c651f4660..7013863c5c 100644 --- a/libs/common/src/admin-console/services/organization/organization.service.ts +++ b/libs/common/src/admin-console/services/organization/organization.service.ts @@ -73,18 +73,18 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti return this.organizations$.pipe(mapToSingleOrganization(id)); } + getAll$(userId?: UserId): Observable { + return this.getOrganizationsFromState$(userId); + } + async getAll(userId?: string): Promise { return await firstValueFrom(this.getOrganizationsFromState$(userId as UserId)); } - async canManageSponsorships(): Promise { - return await firstValueFrom( - this.organizations$.pipe( - mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), - mapToBooleanHasAnyOrganizations(), - ), - ); - } + canManageSponsorships$ = this.organizations$.pipe( + mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(), + mapToBooleanHasAnyOrganizations(), + ); async hasOrganizations(): Promise { return await firstValueFrom(this.organizations$.pipe(mapToBooleanHasAnyOrganizations())); diff --git a/libs/common/src/admin-console/services/policy/policy.service.spec.ts b/libs/common/src/admin-console/services/policy/policy.service.spec.ts index 8fa79f4d1c..88264d1c3b 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.spec.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.spec.ts @@ -32,7 +32,8 @@ describe("PolicyService", () => { organizationService = mock(); activeUserState = stateProvider.activeUser.getFake(POLICIES); - organizationService.organizations$ = of([ + + const organizations$ = of([ // User organization("org1", true, true, OrganizationUserStatusType.Confirmed, false), // Owner @@ -50,8 +51,14 @@ describe("PolicyService", () => { organization("org4", true, true, OrganizationUserStatusType.Confirmed, false), // Another User organization("org5", true, true, OrganizationUserStatusType.Confirmed, false), + // Can manage policies + organization("org6", true, true, OrganizationUserStatusType.Confirmed, true), ]); + organizationService.organizations$ = organizations$; + + organizationService.getAll$.mockReturnValue(organizations$); + policyService = new PolicyService(stateProvider, organizationService); }); @@ -102,66 +109,6 @@ describe("PolicyService", () => { ]); }); - describe("clear", () => { - beforeEach(() => { - activeUserState.nextState( - arrayToRecord([ - policyData("1", "test-organization", PolicyType.MaximumVaultTimeout, true, { - minutes: 14, - }), - ]), - ); - }); - - it("clears state for the active user", async () => { - await policyService.clear(); - - expect(await firstValueFrom(policyService.policies$)).toEqual([]); - expect(await firstValueFrom(activeUserState.state$)).toEqual(null); - expect(stateProvider.activeUser.getFake(POLICIES).nextMock).toHaveBeenCalledWith([ - "userId", - null, - ]); - }); - - it("clears state for an inactive user", async () => { - const inactiveUserId = "someOtherUserId" as UserId; - const inactiveUserState = stateProvider.singleUser.getFake(inactiveUserId, POLICIES); - inactiveUserState.nextState( - arrayToRecord([ - policyData("10", "another-test-organization", PolicyType.PersonalOwnership, true), - ]), - ); - - await policyService.clear(inactiveUserId); - - // Active user is not affected - const expectedActiveUserPolicy: Partial = { - id: "1" as PolicyId, - organizationId: "test-organization", - type: PolicyType.MaximumVaultTimeout, - enabled: true, - data: { minutes: 14 }, - }; - expect(await firstValueFrom(policyService.policies$)).toEqual([expectedActiveUserPolicy]); - expect(await firstValueFrom(activeUserState.state$)).toEqual({ - "1": expectedActiveUserPolicy, - }); - expect(stateProvider.activeUser.getFake(POLICIES).nextMock).not.toHaveBeenCalled(); - - // Non-active user is cleared - expect( - await firstValueFrom( - policyService.getAll$(PolicyType.PersonalOwnership, "someOtherUserId" as UserId), - ), - ).toEqual([]); - expect(await firstValueFrom(inactiveUserState.state$)).toEqual(null); - expect( - stateProvider.singleUser.getFake("someOtherUserId" as UserId, POLICIES).nextMock, - ).toHaveBeenCalledWith(null); - }); - }); - describe("masterPasswordPolicyOptions", () => { it("returns default policy options", async () => { const data: any = { @@ -314,6 +261,22 @@ describe("PolicyService", () => { expect(result).toBeNull(); }); + it.each([ + ["owners", "org2"], + ["administrators", "org6"], + ])("returns the password generator policy for %s", async (_, organization) => { + activeUserState.nextState( + arrayToRecord([ + policyData("policy1", "org1", PolicyType.ActivateAutofill, false), + policyData("policy2", organization, PolicyType.PasswordGenerator, true), + ]), + ); + + const result = await firstValueFrom(policyService.get$(PolicyType.PasswordGenerator)); + + expect(result).toBeTruthy(); + }); + it("does not return policies for organizations that do not use policies", async () => { activeUserState.nextState( arrayToRecord([ diff --git a/libs/common/src/admin-console/services/policy/policy.service.ts b/libs/common/src/admin-console/services/policy/policy.service.ts index d60d2e3293..e36902cbf9 100644 --- a/libs/common/src/admin-console/services/policy/policy.service.ts +++ b/libs/common/src/admin-console/services/policy/policy.service.ts @@ -1,6 +1,6 @@ import { combineLatest, firstValueFrom, map, Observable, of } from "rxjs"; -import { KeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; +import { UserKeyDefinition, POLICIES_DISK, StateProvider } from "../../../platform/state"; import { PolicyId, UserId } from "../../../types/guid"; import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction"; @@ -14,8 +14,9 @@ import { ResetPasswordPolicyOptions } from "../../models/domain/reset-password-p const policyRecordToArray = (policiesMap: { [id: string]: PolicyData }) => Object.values(policiesMap || {}).map((f) => new Policy(f)); -export const POLICIES = KeyDefinition.record(POLICIES_DISK, "policies", { +export const POLICIES = UserKeyDefinition.record(POLICIES_DISK, "policies", { deserializer: (policyData) => policyData, + clearOn: ["logout"], }); export class PolicyService implements InternalPolicyServiceAbstraction { @@ -50,7 +51,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction { map((policies) => policies.filter((p) => p.type === policyType)), ); - return combineLatest([filteredPolicies$, this.organizationService.organizations$]).pipe( + return combineLatest([filteredPolicies$, this.organizationService.getAll$(userId)]).pipe( map(([policies, organizations]) => this.enforcedPolicyFilter(policies, organizations)), ); } @@ -222,10 +223,6 @@ export class PolicyService implements InternalPolicyServiceAbstraction { await this.activeUserPolicyState.update(() => policies); } - async clear(userId?: UserId): Promise { - await this.stateProvider.setUserState(POLICIES, null, userId); - } - /** * Determines whether an orgUser is exempt from a specific policy because of their role * Generally orgUsers who can manage policies are exempt from them, but some policies are stricter @@ -235,6 +232,9 @@ export class PolicyService implements InternalPolicyServiceAbstraction { case PolicyType.MaximumVaultTimeout: // Max Vault Timeout applies to everyone except owners return organization.isOwner; + case PolicyType.PasswordGenerator: + // password generation policy applies to everyone + return false; default: return organization.canManagePolicies; } diff --git a/libs/common/src/admin-console/services/provider.service.ts b/libs/common/src/admin-console/services/provider.service.ts index 47291a5520..064e0c7175 100644 --- a/libs/common/src/admin-console/services/provider.service.ts +++ b/libs/common/src/admin-console/services/provider.service.ts @@ -1,13 +1,14 @@ import { Observable, map, firstValueFrom, of, switchMap, take } from "rxjs"; -import { KeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state"; +import { UserKeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service"; import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; -export const PROVIDERS = KeyDefinition.record(PROVIDERS_DISK, "providers", { +export const PROVIDERS = UserKeyDefinition.record(PROVIDERS_DISK, "providers", { deserializer: (obj: ProviderData) => obj, + clearOn: ["logout"], }); function mapToSingleProvider(providerId: string) { diff --git a/libs/common/src/admin-console/services/provider/provider-api.service.ts b/libs/common/src/admin-console/services/provider/provider-api.service.ts new file mode 100644 index 0000000000..2ee921393f --- /dev/null +++ b/libs/common/src/admin-console/services/provider/provider-api.service.ts @@ -0,0 +1,47 @@ +import { ApiService } from "../../../abstractions/api.service"; +import { ProviderApiServiceAbstraction } from "../../abstractions/provider/provider-api.service.abstraction"; +import { ProviderSetupRequest } from "../../models/request/provider/provider-setup.request"; +import { ProviderUpdateRequest } from "../../models/request/provider/provider-update.request"; +import { ProviderVerifyRecoverDeleteRequest } from "../../models/request/provider/provider-verify-recover-delete.request"; +import { ProviderResponse } from "../../models/response/provider/provider.response"; + +export class ProviderApiService implements ProviderApiServiceAbstraction { + constructor(private apiService: ApiService) {} + async postProviderSetup(id: string, request: ProviderSetupRequest) { + const r = await this.apiService.send( + "POST", + "/providers/" + id + "/setup", + request, + true, + true, + ); + return new ProviderResponse(r); + } + + async getProvider(id: string) { + const r = await this.apiService.send("GET", "/providers/" + id, null, true, true); + return new ProviderResponse(r); + } + + async putProvider(id: string, request: ProviderUpdateRequest) { + const r = await this.apiService.send("PUT", "/providers/" + id, request, true, true); + return new ProviderResponse(r); + } + + providerRecoverDeleteToken( + providerId: string, + request: ProviderVerifyRecoverDeleteRequest, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/delete-recover-token", + request, + false, + false, + ); + } + + async deleteProvider(id: string): Promise { + await this.apiService.send("DELETE", "/providers/" + id, null, true, false); + } +} diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index 4e2a462755..fa9ad36378 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -1,27 +1,23 @@ import { Observable } from "rxjs"; import { UserId } from "../../types/guid"; -import { AuthenticationStatus } from "../enums/authentication-status"; /** * Holds information about an account for use in the AccountService * if more information is added, be sure to update the equality method. */ export type AccountInfo = { - status: AuthenticationStatus; email: string; name: string | undefined; }; export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { - return a?.status === b?.status && a?.email === b?.email && a?.name === b?.name; + return a?.email === b?.email && a?.name === b?.name; } export abstract class AccountService { accounts$: Observable>; activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; - accountLock$: Observable; - accountLogout$: Observable; /** * Updates the `accounts$` observable with the new account data. * @param userId @@ -40,24 +36,6 @@ export abstract class AccountService { * @param email */ abstract setAccountEmail(userId: UserId, email: string): Promise; - /** - * Updates the `accounts$` observable with the new account status. - * Also emits the `accountLock$` or `accountLogout$` observable if the status is `Locked` or `LoggedOut` respectively. - * @param userId - * @param status - */ - abstract setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise; - /** - * Updates the `accounts$` observable with the new account status if the current status is higher than the `maxStatus`. - * - * This method only downgrades status to the maximum value sent in, it will not increase authentication status. - * - * @example An account is transitioning from unlocked to logged out. If callbacks that set the status to locked occur - * after it is updated to logged out, the account will be in the incorrect state. - * @param userId The user id of the account to be updated. - * @param maxStatus The new status of the account. - */ - abstract setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise; /** * Updates the `activeAccount$` observable with the new active account. * @param userId diff --git a/libs/common/src/auth/abstractions/anonymous-hub.service.ts b/libs/common/src/auth/abstractions/anonymous-hub.service.ts index 43bdabd512..e108dccbb6 100644 --- a/libs/common/src/auth/abstractions/anonymous-hub.service.ts +++ b/libs/common/src/auth/abstractions/anonymous-hub.service.ts @@ -1,4 +1,4 @@ export abstract class AnonymousHubService { - createHubConnection: (token: string) => void; - stopHubConnection: () => void; + createHubConnection: (token: string) => Promise; + stopHubConnection: () => Promise; } diff --git a/libs/common/src/auth/abstractions/auth.service.ts b/libs/common/src/auth/abstractions/auth.service.ts index dc51e2fdb0..36d5d219b2 100644 --- a/libs/common/src/auth/abstractions/auth.service.ts +++ b/libs/common/src/auth/abstractions/auth.service.ts @@ -1,6 +1,20 @@ +import { Observable } from "rxjs"; + +import { UserId } from "../../types/guid"; import { AuthenticationStatus } from "../enums/authentication-status"; export abstract class AuthService { - getAuthStatus: (userId?: string) => Promise; - logOut: (callback: () => void) => void; + /** Authentication status for the active user */ + abstract activeAccountStatus$: Observable; + /** Authentication status for all known users */ + abstract authStatuses$: Observable>; + /** + * Returns an observable authentication status for the given user id. + * @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut` + * @param userId The user id to check for an access token. + */ + abstract authStatusFor$(userId: UserId): Observable; + /** @deprecated use {@link activeAccountStatus$} instead */ + abstract getAuthStatus: (userId?: string) => Promise; + abstract logOut: (callback: () => void) => void; } diff --git a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts index 415355cfc7..53fe214035 100644 --- a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts @@ -1,6 +1,7 @@ import { Observable } from "rxjs"; import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; @@ -10,17 +11,24 @@ export abstract class DeviceTrustCryptoServiceAbstraction { * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset */ - getShouldTrustDevice: () => Promise; - setShouldTrustDevice: (value: boolean) => Promise; + getShouldTrustDevice: (userId: UserId) => Promise; + setShouldTrustDevice: (userId: UserId, value: boolean) => Promise; - trustDeviceIfRequired: () => Promise; + trustDeviceIfRequired: (userId: UserId) => Promise; - trustDevice: () => Promise; - getDeviceKey: () => Promise; + trustDevice: (userId: UserId) => Promise; + + /** Retrieves the device key if it exists from state or secure storage if supported for the active user. */ + getDeviceKey: (userId: UserId) => Promise; decryptUserKeyWithDeviceKey: ( + userId: UserId, encryptedDevicePrivateKey: EncString, encryptedUserKey: EncString, - deviceKey?: DeviceKey, + deviceKey: DeviceKey, ) => Promise; - rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise; + rotateDevicesTrust: ( + userId: UserId, + newUserKey: UserKey, + masterPasswordHash: string, + ) => Promise; } diff --git a/libs/common/src/auth/abstractions/key-connector.service.ts b/libs/common/src/auth/abstractions/key-connector.service.ts index b7c8d5d0d0..36f413d70c 100644 --- a/libs/common/src/auth/abstractions/key-connector.service.ts +++ b/libs/common/src/auth/abstractions/key-connector.service.ts @@ -15,5 +15,4 @@ export abstract class KeyConnectorService { setConvertAccountRequired: (status: boolean) => Promise; getConvertAccountRequired: () => Promise; removeConvertAccountRequired: () => Promise; - clear: () => Promise; } diff --git a/libs/common/src/auth/abstractions/login.service.ts b/libs/common/src/auth/abstractions/login.service.ts deleted file mode 100644 index 9a884fd5d1..0000000000 --- a/libs/common/src/auth/abstractions/login.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -export abstract class LoginService { - getEmail: () => string; - getRememberEmail: () => boolean; - setEmail: (value: string) => void; - setRememberEmail: (value: boolean) => void; - clearValues: () => void; - saveEmailSettings: () => Promise; -} diff --git a/libs/common/src/auth/abstractions/master-password.service.abstraction.ts b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts new file mode 100644 index 0000000000..b36c8bfaae --- /dev/null +++ b/libs/common/src/auth/abstractions/master-password.service.abstraction.ts @@ -0,0 +1,82 @@ +import { Observable } from "rxjs"; + +import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; +import { MasterKey } from "../../types/key"; +import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason"; + +export abstract class MasterPasswordServiceAbstraction { + /** + * An observable that emits if the user is being forced to set a password on login and why. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract forceSetPasswordReason$: (userId: UserId) => Observable; + /** + * An observable that emits the master key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract masterKey$: (userId: UserId) => Observable; + /** + * An observable that emits the master key hash for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract masterKeyHash$: (userId: UserId) => Observable; + /** + * Returns the master key encrypted user key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise; +} + +export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { + /** + * Set the master key for the user. + * Note: Use {@link clearMasterKey} to clear the master key. + * @param masterKey The master key. + * @param userId The user ID. + * @throws If the user ID or master key is missing. + */ + abstract setMasterKey: (masterKey: MasterKey, userId: UserId) => Promise; + /** + * Clear the master key for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract clearMasterKey: (userId: UserId) => Promise; + /** + * Set the master key hash for the user. + * Note: Use {@link clearMasterKeyHash} to clear the master key hash. + * @param masterKeyHash The master key hash. + * @param userId The user ID. + * @throws If the user ID or master key hash is missing. + */ + abstract setMasterKeyHash: (masterKeyHash: string, userId: UserId) => Promise; + /** + * Clear the master key hash for the user. + * @param userId The user ID. + * @throws If the user ID is missing. + */ + abstract clearMasterKeyHash: (userId: UserId) => Promise; + + /** + * Set the master key encrypted user key for the user. + * @param encryptedKey The master key encrypted user key. + * @param userId The user ID. + * @throws If the user ID or encrypted key is missing. + */ + abstract setMasterKeyEncryptedUserKey: (encryptedKey: EncString, userId: UserId) => Promise; + /** + * Set the force set password reason for the user. + * @param reason The reason the user is being forced to set a password. + * @param userId The user ID. + * @throws If the user ID or reason is missing. + */ + abstract setForceSetPasswordReason: ( + reason: ForceSetPasswordReason, + userId: UserId, + ) => Promise; +} diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index d2358314d7..75bb383882 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -1,8 +1,15 @@ +import { Observable } from "rxjs"; + import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { UserId } from "../../types/guid"; import { DecodedAccessToken } from "../services/token.service"; export abstract class TokenService { + /** + * Returns an observable that emits a boolean indicating whether the user has an access token. + * @param userId The user id to check for an access token. + */ + abstract hasAccessToken$(userId: UserId): Observable; /** * Sets the access token, refresh token, API Key Client ID, and API Key Client Secret in memory or disk * based on the given vaultTimeoutAction and vaultTimeout and the derived access token user id. @@ -10,17 +17,18 @@ export abstract class TokenService { * Note 2: this method also enforces always setting the access token and the refresh token together as * we can retrieve the user id required to set the refresh token from the access token for efficiency. * @param accessToken The access token to set. - * @param refreshToken The refresh token to set. - * @param clientIdClientSecret The API Key Client ID and Client Secret to set. * @param vaultTimeoutAction The action to take when the vault times out. * @param vaultTimeout The timeout for the vault. + * @param refreshToken The optional refresh token to set. Note: this is undefined when using the CLI Login Via API Key flow + * @param clientIdClientSecret The API Key Client ID and Client Secret to set. + * * @returns A promise that resolves when the tokens have been set. */ setTokens: ( accessToken: string, - refreshToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, + refreshToken?: string, clientIdClientSecret?: [string, string], ) => Promise; diff --git a/libs/common/src/auth/models/domain/admin-auth-req-storable.ts b/libs/common/src/auth/models/domain/admin-auth-req-storable.ts index 1eae7eeab1..df0341ac16 100644 --- a/libs/common/src/auth/models/domain/admin-auth-req-storable.ts +++ b/libs/common/src/auth/models/domain/admin-auth-req-storable.ts @@ -1,11 +1,7 @@ +import { Jsonify } from "type-fest"; + import { Utils } from "../../../platform/misc/utils"; -// TODO: Tech Debt: potentially create a type Storage shape vs using a class here in the future -// type StorageShape { -// id: string; -// privateKey: string; -// } -// so we can get rid of the any type passed into fromJSON and coming out of ToJSON export class AdminAuthRequestStorable { id: string; privateKey: Uint8Array; @@ -23,7 +19,7 @@ export class AdminAuthRequestStorable { }; } - static fromJSON(obj: any): AdminAuthRequestStorable { + static fromJSON(obj: Jsonify): AdminAuthRequestStorable { if (obj == null) { return null; } diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index e4195365f4..a9cec82c51 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -8,7 +8,6 @@ import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { UserId } from "../../types/guid"; import { AccountInfo } from "../abstractions/account.service"; -import { AuthenticationStatus } from "../enums/authentication-status"; import { ACCOUNT_ACCOUNTS, @@ -24,9 +23,7 @@ describe("accountService", () => { let accountsState: FakeGlobalState>; let activeAccountIdState: FakeGlobalState; const userId = "userId" as UserId; - function userInfo(status: AuthenticationStatus): AccountInfo { - return { status, email: "email", name: "name" }; - } + const userInfo = { email: "email", name: "name" }; beforeEach(() => { messagingService = mock(); @@ -50,61 +47,49 @@ describe("accountService", () => { expect(emissions).toEqual([undefined]); }); - it("should emit the active account and status", async () => { + it("should emit the active account", async () => { const emissions = trackEmissions(sut.activeAccount$); - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); expect(emissions).toEqual([ undefined, // initial value - { id: userId, ...userInfo(AuthenticationStatus.Unlocked) }, - ]); - }); - - it("should update the status if the account status changes", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); - activeAccountIdState.stateSubject.next(userId); - const emissions = trackEmissions(sut.activeAccount$); - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) }); - - expect(emissions).toEqual([ - { id: userId, ...userInfo(AuthenticationStatus.Unlocked) }, - { id: userId, ...userInfo(AuthenticationStatus.Locked) }, + { id: userId, ...userInfo }, ]); }); it("should remember the last emitted value", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); expect(await firstValueFrom(sut.activeAccount$)).toEqual({ id: userId, - ...userInfo(AuthenticationStatus.Unlocked), + ...userInfo, }); }); }); describe("accounts$", () => { it("should maintain an accounts cache", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Locked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); + accountsState.stateSubject.next({ [userId]: userInfo }); expect(await firstValueFrom(sut.accounts$)).toEqual({ - [userId]: userInfo(AuthenticationStatus.Locked), + [userId]: userInfo, }); }); }); describe("addAccount", () => { it("should emit the new account", async () => { - await sut.addAccount(userId, userInfo(AuthenticationStatus.Unlocked)); + await sut.addAccount(userId, userInfo); const currentValue = await firstValueFrom(sut.accounts$); - expect(currentValue).toEqual({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + expect(currentValue).toEqual({ [userId]: userInfo }); }); }); describe("setAccountName", () => { - const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; + const initialState = { [userId]: userInfo }; beforeEach(() => { accountsState.stateSubject.next(initialState); }); @@ -114,7 +99,7 @@ describe("accountService", () => { const currentState = await firstValueFrom(accountsState.state$); expect(currentState).toEqual({ - [userId]: { ...userInfo(AuthenticationStatus.Unlocked), name: "new name" }, + [userId]: { ...userInfo, name: "new name" }, }); }); @@ -127,7 +112,7 @@ describe("accountService", () => { }); describe("setAccountEmail", () => { - const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; + const initialState = { [userId]: userInfo }; beforeEach(() => { accountsState.stateSubject.next(initialState); }); @@ -137,7 +122,7 @@ describe("accountService", () => { const currentState = await firstValueFrom(accountsState.state$); expect(currentState).toEqual({ - [userId]: { ...userInfo(AuthenticationStatus.Unlocked), email: "new email" }, + [userId]: { ...userInfo, email: "new email" }, }); }); @@ -149,49 +134,9 @@ describe("accountService", () => { }); }); - describe("setAccountStatus", () => { - const initialState = { [userId]: userInfo(AuthenticationStatus.Unlocked) }; - beforeEach(() => { - accountsState.stateSubject.next(initialState); - }); - - it("should update the account", async () => { - await sut.setAccountStatus(userId, AuthenticationStatus.Locked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual({ - [userId]: { - ...userInfo(AuthenticationStatus.Unlocked), - status: AuthenticationStatus.Locked, - }, - }); - }); - - it("should not update if the status is the same", async () => { - await sut.setAccountStatus(userId, AuthenticationStatus.Unlocked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual(initialState); - }); - - it("should emit logout if the status is logged out", async () => { - const emissions = trackEmissions(sut.accountLogout$); - await sut.setAccountStatus(userId, AuthenticationStatus.LoggedOut); - - expect(emissions).toEqual([userId]); - }); - - it("should emit lock if the status is locked", async () => { - const emissions = trackEmissions(sut.accountLock$); - await sut.setAccountStatus(userId, AuthenticationStatus.Locked); - - expect(emissions).toEqual([userId]); - }); - }); - describe("switchAccount", () => { beforeEach(() => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); + accountsState.stateSubject.next({ [userId]: userInfo }); activeAccountIdState.stateSubject.next(userId); }); @@ -207,26 +152,4 @@ describe("accountService", () => { expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); }); }); - - describe("setMaxAccountStatus", () => { - it("should update the account", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.Unlocked) }); - await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual({ - [userId]: userInfo(AuthenticationStatus.Locked), - }); - }); - - it("should not update if the new max status is higher than the current", async () => { - accountsState.stateSubject.next({ [userId]: userInfo(AuthenticationStatus.LoggedOut) }); - await sut.setMaxAccountStatus(userId, AuthenticationStatus.Locked); - const currentState = await firstValueFrom(accountsState.state$); - - expect(currentState).toEqual({ - [userId]: userInfo(AuthenticationStatus.LoggedOut), - }); - }); - }); }); diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 8ef235d815..77d61fae91 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -14,7 +14,6 @@ import { KeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; -import { AuthenticationStatus } from "../enums/authentication-status"; export const ACCOUNT_ACCOUNTS = KeyDefinition.record( ACCOUNT_MEMORY, @@ -36,8 +35,6 @@ export class AccountServiceImplementation implements InternalAccountService { accounts$; activeAccount$; - accountLock$ = this.lock.asObservable(); - accountLogout$ = this.logout.asObservable(); constructor( private messagingService: MessagingService, @@ -74,34 +71,6 @@ export class AccountServiceImplementation implements InternalAccountService { await this.setAccountInfo(userId, { email }); } - async setAccountStatus(userId: UserId, status: AuthenticationStatus): Promise { - await this.setAccountInfo(userId, { status }); - - if (status === AuthenticationStatus.LoggedOut) { - this.logout.next(userId); - } else if (status === AuthenticationStatus.Locked) { - this.lock.next(userId); - } - } - - async setMaxAccountStatus(userId: UserId, maxStatus: AuthenticationStatus): Promise { - await this.accountsState.update( - (accounts) => { - accounts[userId].status = maxStatus; - return accounts; - }, - { - shouldUpdate: (accounts) => { - if (accounts?.[userId] == null) { - throw new Error("Account does not exist"); - } - - return accounts[userId].status > maxStatus; - }, - }, - ); - } - async switchAccount(userId: UserId): Promise { await this.activeAccountIdState.update( (_, accounts) => { diff --git a/libs/common/src/auth/services/anonymous-hub.service.ts b/libs/common/src/auth/services/anonymous-hub.service.ts index fe8ae64183..747fbc3917 100644 --- a/libs/common/src/auth/services/anonymous-hub.service.ts +++ b/libs/common/src/auth/services/anonymous-hub.service.ts @@ -7,13 +7,13 @@ import { import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; import { firstValueFrom } from "rxjs"; -import { LoginStrategyServiceAbstraction } from "../../../../auth/src/common/abstractions/login-strategy.service"; +import { AuthRequestServiceAbstraction } from "../../../../auth/src/common/abstractions"; +import { NotificationType } from "../../enums"; import { AuthRequestPushNotification, NotificationResponse, } from "../../models/response/notification.response"; import { EnvironmentService } from "../../platform/abstractions/environment.service"; -import { LogService } from "../../platform/abstractions/log.service"; import { AnonymousHubService as AnonymousHubServiceAbstraction } from "../abstractions/anonymous-hub.service"; export class AnonymousHubService implements AnonymousHubServiceAbstraction { @@ -22,8 +22,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction { constructor( private environmentService: EnvironmentService, - private loginStrategyService: LoginStrategyServiceAbstraction, - private logService: LogService, + private authRequestService: AuthRequestServiceAbstraction, ) {} async createHubConnection(token: string) { @@ -37,26 +36,25 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction { .withHubProtocol(new MessagePackHubProtocol() as IHubProtocol) .build(); - this.anonHubConnection.start().catch((error) => this.logService.error(error)); + await this.anonHubConnection.start(); this.anonHubConnection.on("AuthRequestResponseRecieved", (data: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises this.ProcessNotification(new NotificationResponse(data)); }); } - stopHubConnection() { + async stopHubConnection() { if (this.anonHubConnection) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.anonHubConnection.stop(); + await this.anonHubConnection.stop(); } } - private async ProcessNotification(notification: NotificationResponse) { - await this.loginStrategyService.sendAuthRequestPushNotification( - notification.payload as AuthRequestPushNotification, - ); + private ProcessNotification(notification: NotificationResponse) { + switch (notification.type) { + case NotificationType.AuthRequestResponse: + this.authRequestService.sendAuthRequestPushNotification( + notification.payload as AuthRequestPushNotification, + ); + } } } diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts new file mode 100644 index 0000000000..3bdf85d3e1 --- /dev/null +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -0,0 +1,180 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { + FakeAccountService, + makeStaticByteArray, + mockAccountServiceWith, + trackEmissions, +} from "../../../spec"; +import { ApiService } from "../../abstractions/api.service"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { UserId } from "../../types/guid"; +import { UserKey } from "../../types/key"; +import { TokenService } from "../abstractions/token.service"; +import { AuthenticationStatus } from "../enums/authentication-status"; + +import { AuthService } from "./auth.service"; + +describe("AuthService", () => { + let sut: AuthService; + + let accountService: FakeAccountService; + let messagingService: MockProxy; + let cryptoService: MockProxy; + let apiService: MockProxy; + let stateService: MockProxy; + let tokenService: MockProxy; + + const userId = Utils.newGuid() as UserId; + const userKey = new SymmetricCryptoKey(makeStaticByteArray(32) as Uint8Array) as UserKey; + + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + messagingService = mock(); + cryptoService = mock(); + apiService = mock(); + stateService = mock(); + tokenService = mock(); + + sut = new AuthService( + accountService, + messagingService, + cryptoService, + apiService, + stateService, + tokenService, + ); + }); + + describe("activeAccountStatus$", () => { + const accountInfo = { + status: AuthenticationStatus.Unlocked, + id: userId, + email: "email", + name: "name", + }; + + beforeEach(() => { + accountService.activeAccountSubject.next(accountInfo); + tokenService.hasAccessToken$.mockReturnValue(of(true)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + }); + + it("emits LoggedOut when there is no active account", async () => { + accountService.activeAccountSubject.next(undefined); + + expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual( + AuthenticationStatus.LoggedOut, + ); + }); + + it("emits LoggedOut when there is no access token", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(false)); + + expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual( + AuthenticationStatus.LoggedOut, + ); + }); + + it("emits LoggedOut when there is no access token but has a user key", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(false)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + + expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual( + AuthenticationStatus.LoggedOut, + ); + }); + + it("emits Locked when there is an access token and no user key", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(true)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + + expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Locked); + }); + + it("emits Unlocked when there is an access token and user key", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(true)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + + expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Unlocked); + }); + + it("follows the current active user", async () => { + const accountInfo2 = { + status: AuthenticationStatus.Unlocked, + id: Utils.newGuid() as UserId, + email: "email2", + name: "name2", + }; + + const emissions = trackEmissions(sut.activeAccountStatus$); + + tokenService.hasAccessToken$.mockReturnValue(of(true)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + accountService.activeAccountSubject.next(accountInfo2); + + expect(emissions).toEqual([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked]); + }); + }); + + describe("authStatuses$", () => { + it("requests auth status for all known users", async () => { + const userId2 = Utils.newGuid() as UserId; + + await accountService.addAccount(userId2, { email: "email2", name: "name2" }); + + const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked)); + sut.authStatusFor$ = mockFn; + + await expect(firstValueFrom(await sut.authStatuses$)).resolves.toEqual({ + [userId]: AuthenticationStatus.Locked, + [userId2]: AuthenticationStatus.Locked, + }); + expect(mockFn).toHaveBeenCalledTimes(2); + expect(mockFn).toHaveBeenCalledWith(userId); + expect(mockFn).toHaveBeenCalledWith(userId2); + }); + }); + + describe("authStatusFor$", () => { + beforeEach(() => { + tokenService.hasAccessToken$.mockReturnValue(of(true)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + }); + + it("emits LoggedOut when userId is null", async () => { + expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual( + AuthenticationStatus.LoggedOut, + ); + }); + + it("emits LoggedOut when there is no access token", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(false)); + + expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual( + AuthenticationStatus.LoggedOut, + ); + }); + + it("emits Locked when there is an access token and no user key", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(true)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + + expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(AuthenticationStatus.Locked); + }); + + it("emits Unlocked when there is an access token and user key", async () => { + tokenService.hasAccessToken$.mockReturnValue(of(true)); + cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + + expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual( + AuthenticationStatus.Unlocked, + ); + }); + }); +}); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 14d49956a4..7a29d313e7 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,18 +1,88 @@ +import { + Observable, + combineLatest, + distinctUntilChanged, + map, + of, + shareReplay, + switchMap, +} from "rxjs"; + import { ApiService } from "../../abstractions/api.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { StateService } from "../../platform/abstractions/state.service"; import { KeySuffixOptions } from "../../platform/enums"; +import { UserId } from "../../types/guid"; +import { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; +import { TokenService } from "../abstractions/token.service"; import { AuthenticationStatus } from "../enums/authentication-status"; export class AuthService implements AuthServiceAbstraction { + activeAccountStatus$: Observable; + authStatuses$: Observable>; + constructor( + protected accountService: AccountService, protected messagingService: MessagingService, protected cryptoService: CryptoService, protected apiService: ApiService, protected stateService: StateService, - ) {} + private tokenService: TokenService, + ) { + this.activeAccountStatus$ = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((userId) => { + return this.authStatusFor$(userId); + }), + ); + + this.authStatuses$ = this.accountService.accounts$.pipe( + map((accounts) => Object.keys(accounts) as UserId[]), + switchMap((entries) => + combineLatest( + entries.map((userId) => + this.authStatusFor$(userId).pipe(map((status) => ({ userId, status }))), + ), + ), + ), + map((statuses) => { + return statuses.reduce( + (acc, { userId, status }) => { + acc[userId] = status; + return acc; + }, + {} as Record, + ); + }), + ); + } + + authStatusFor$(userId: UserId): Observable { + if (userId == null) { + return of(AuthenticationStatus.LoggedOut); + } + + return combineLatest([ + this.cryptoService.getInMemoryUserKeyFor$(userId), + this.tokenService.hasAccessToken$(userId), + ]).pipe( + map(([userKey, hasAccessToken]) => { + if (!hasAccessToken) { + return AuthenticationStatus.LoggedOut; + } + + if (!userKey) { + return AuthenticationStatus.Locked; + } + + return AuthenticationStatus.Unlocked; + }), + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: false }), + ); + } async getAuthStatus(userId?: string): Promise { // If we don't have an access token or userId, we're logged out diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts index 71f83f07c3..6fb58eab28 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts @@ -9,9 +9,13 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; -import { StateService } from "../../platform/abstractions/state.service"; +import { AbstractStorageService } from "../../platform/abstractions/storage.service"; +import { StorageLocation } from "../../platform/enums"; import { EncString } from "../../platform/models/domain/enc-string"; +import { StorageOptions } from "../../platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { UserKey, DeviceKey } from "../../types/key"; import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; @@ -22,7 +26,27 @@ import { UpdateDevicesTrustRequest, } from "../models/request/update-devices-trust.request"; +/** Uses disk storage so that the device key can persist after log out and tab removal. */ +export const DEVICE_KEY = new UserKeyDefinition(DEVICE_TRUST_DISK_LOCAL, "deviceKey", { + deserializer: (deviceKey) => SymmetricCryptoKey.fromJSON(deviceKey) as DeviceKey, + clearOn: [], // Device key is needed to log back into device, so we can't clear it automatically during lock or logout +}); + +/** Uses disk storage so that the shouldTrustDevice bool can persist across login. */ +export const SHOULD_TRUST_DEVICE = new UserKeyDefinition( + DEVICE_TRUST_DISK_LOCAL, + "shouldTrustDevice", + { + deserializer: (shouldTrustDevice) => shouldTrustDevice, + clearOn: [], // Need to preserve the user setting, so we can't clear it automatically during lock or logout + }, +); + export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { + private readonly platformSupportsSecureStorage = + this.platformUtilsService.supportsSecureStorage(); + private readonly deviceKeySecureStorageKey: string = "_deviceKey"; + supportsDeviceTrust$: Observable; constructor( @@ -30,11 +54,12 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, private encryptService: EncryptService, - private stateService: StateService, private appIdService: AppIdService, private devicesApiService: DevicesApiServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private stateProvider: StateProvider, + private secureStorageService: AbstractStorageService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( @@ -46,24 +71,44 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset */ - async getShouldTrustDevice(): Promise { - return await this.stateService.getShouldTrustDevice(); + async getShouldTrustDevice(userId: UserId): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot get should trust device."); + } + + const shouldTrustDevice = await firstValueFrom( + this.stateProvider.getUserState$(SHOULD_TRUST_DEVICE, userId), + ); + + return shouldTrustDevice; } - async setShouldTrustDevice(value: boolean): Promise { - await this.stateService.setShouldTrustDevice(value); + async setShouldTrustDevice(userId: UserId, value: boolean): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot set should trust device."); + } + + await this.stateProvider.setUserState(SHOULD_TRUST_DEVICE, value, userId); } - async trustDeviceIfRequired(): Promise { - const shouldTrustDevice = await this.getShouldTrustDevice(); + async trustDeviceIfRequired(userId: UserId): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot trust device if required."); + } + + const shouldTrustDevice = await this.getShouldTrustDevice(userId); if (shouldTrustDevice) { - await this.trustDevice(); + await this.trustDevice(userId); // reset the trust choice - await this.setShouldTrustDevice(false); + await this.setShouldTrustDevice(userId, false); } } - async trustDevice(): Promise { + async trustDevice(userId: UserId): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot trust device."); + } + // Attempt to get user key const userKey: UserKey = await this.cryptoService.getUserKey(); @@ -104,15 +149,23 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac ); // store device key in local/secure storage if enc keys posted to server successfully - await this.setDeviceKey(deviceKey); + await this.setDeviceKey(userId, deviceKey); this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted")); return deviceResponse; } - async rotateDevicesTrust(newUserKey: UserKey, masterPasswordHash: string): Promise { - const currentDeviceKey = await this.getDeviceKey(); + async rotateDevicesTrust( + userId: UserId, + newUserKey: UserKey, + masterPasswordHash: string, + ): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot rotate device's trust."); + } + + const currentDeviceKey = await this.getDeviceKey(userId); if (currentDeviceKey == null) { // If the current device doesn't have a device key available to it, then we can't // rotate any trust at all, so early return. @@ -165,26 +218,59 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac await this.devicesApiService.updateTrust(trustRequest, deviceIdentifier); } - async getDeviceKey(): Promise { - return await this.stateService.getDeviceKey(); + async getDeviceKey(userId: UserId): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot get device key."); + } + + if (this.platformSupportsSecureStorage) { + const deviceKeyB64 = await this.secureStorageService.get< + ReturnType + >(`${userId}${this.deviceKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); + + const deviceKey = SymmetricCryptoKey.fromJSON(deviceKeyB64) as DeviceKey; + + return deviceKey; + } + + const deviceKey = await firstValueFrom(this.stateProvider.getUserState$(DEVICE_KEY, userId)); + + return deviceKey; } - private async setDeviceKey(deviceKey: DeviceKey | null): Promise { - await this.stateService.setDeviceKey(deviceKey); + private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise { + if (!userId) { + throw new Error("UserId is required. Cannot set device key."); + } + + if (this.platformSupportsSecureStorage) { + await this.secureStorageService.save( + `${userId}${this.deviceKeySecureStorageKey}`, + deviceKey, + this.getSecureStorageOptions(userId), + ); + return; + } + + await this.stateProvider.setUserState(DEVICE_KEY, deviceKey?.toJSON(), userId); } private async makeDeviceKey(): Promise { // Create 512-bit device key - return (await this.keyGenerationService.createKey(512)) as DeviceKey; + const deviceKey = (await this.keyGenerationService.createKey(512)) as DeviceKey; + + return deviceKey; } async decryptUserKeyWithDeviceKey( + userId: UserId, encryptedDevicePrivateKey: EncString, encryptedUserKey: EncString, - deviceKey?: DeviceKey, + deviceKey: DeviceKey, ): Promise { - // If device key provided use it, otherwise try to retrieve from storage - deviceKey ||= await this.getDeviceKey(); + if (!userId) { + throw new Error("UserId is required. Cannot decrypt user key with device key."); + } if (!deviceKey) { // User doesn't have a device key anymore so device is untrusted @@ -207,9 +293,17 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac return new SymmetricCryptoKey(userKey) as UserKey; } catch (e) { // If either decryption effort fails, we want to remove the device key - await this.setDeviceKey(null); + await this.setDeviceKey(userId, null); return null; } } + + private getSecureStorageOptions(userId: UserId): StorageOptions { + return { + storageLocation: StorageLocation.Disk, + useSecureStorage: true, + userId: userId, + }; + } } diff --git a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts index 1d33223ddd..af147b3481 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts @@ -4,6 +4,9 @@ import { BehaviorSubject, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; +import { FakeActiveUserState } from "../../../spec/fake-state"; +import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { DeviceType } from "../../enums"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; @@ -12,18 +15,26 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; -import { StateService } from "../../platform/abstractions/state.service"; +import { AbstractStorageService } from "../../platform/abstractions/storage.service"; +import { StorageLocation } from "../../platform/enums"; import { EncryptionType } from "../../platform/enums/encryption-type.enum"; +import { Utils } from "../../platform/misc/utils"; import { EncString } from "../../platform/models/domain/enc-string"; +import { StorageOptions } from "../../platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "../../types/csprng"; +import { UserId } from "../../types/guid"; import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction"; import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request"; import { ProtectedDeviceResponse } from "../models/response/protected-device.response"; -import { DeviceTrustCryptoService } from "./device-trust-crypto.service.implementation"; +import { + SHOULD_TRUST_DEVICE, + DEVICE_KEY, + DeviceTrustCryptoService, +} from "./device-trust-crypto.service.implementation"; describe("deviceTrustCryptoService", () => { let deviceTrustCryptoService: DeviceTrustCryptoService; @@ -32,33 +43,34 @@ describe("deviceTrustCryptoService", () => { const cryptoFunctionService = mock(); const cryptoService = mock(); const encryptService = mock(); - const stateService = mock(); const appIdService = mock(); const devicesApiService = mock(); const i18nService = mock(); const platformUtilsService = mock(); - const userDecryptionOptionsService = mock(); + const secureStorageService = mock(); + const userDecryptionOptionsService = mock(); const decryptionOptions = new BehaviorSubject(null); + let stateProvider: FakeStateProvider; + + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; + + const deviceKeyPartialSecureStorageKey = "_deviceKey"; + const deviceKeySecureStorageKey = `${mockUserId}${deviceKeyPartialSecureStorageKey}`; + + const secureStorageOptions: StorageOptions = { + storageLocation: StorageLocation.Disk, + useSecureStorage: true, + userId: mockUserId, + }; + beforeEach(() => { jest.clearAllMocks(); - - decryptionOptions.next({} as any); - userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; - - deviceTrustCryptoService = new DeviceTrustCryptoService( - keyGenerationService, - cryptoFunctionService, - cryptoService, - encryptService, - stateService, - appIdService, - devicesApiService, - i18nService, - platformUtilsService, - userDecryptionOptionsService, - ); + const supportsSecureStorage = false; // default to false; tests will override as needed + // By default all the tests will have a mocked active user in state provider. + deviceTrustCryptoService = createDeviceTrustCryptoService(mockUserId, supportsSecureStorage); }); it("instantiates", () => { @@ -67,27 +79,26 @@ describe("deviceTrustCryptoService", () => { describe("User Trust Device Choice For Decryption", () => { describe("getShouldTrustDevice", () => { - it("gets the user trust device choice for decryption from the state service", async () => { - const stateSvcGetShouldTrustDeviceSpy = jest.spyOn(stateService, "getShouldTrustDevice"); + it("gets the user trust device choice for decryption", async () => { + const newValue = true; - const expectedValue = true; - stateSvcGetShouldTrustDeviceSpy.mockResolvedValue(expectedValue); - const result = await deviceTrustCryptoService.getShouldTrustDevice(); + await stateProvider.setUserState(SHOULD_TRUST_DEVICE, newValue, mockUserId); - expect(stateSvcGetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1); - expect(result).toEqual(expectedValue); + const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + + expect(result).toEqual(newValue); }); }); describe("setShouldTrustDevice", () => { - it("sets the user trust device choice for decryption in the state service", async () => { - const stateSvcSetShouldTrustDeviceSpy = jest.spyOn(stateService, "setShouldTrustDevice"); + it("sets the user trust device choice for decryption ", async () => { + await stateProvider.setUserState(SHOULD_TRUST_DEVICE, false, mockUserId); const newValue = true; - await deviceTrustCryptoService.setShouldTrustDevice(newValue); + await deviceTrustCryptoService.setShouldTrustDevice(mockUserId, newValue); - expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledTimes(1); - expect(stateSvcSetShouldTrustDeviceSpy).toHaveBeenCalledWith(newValue); + const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + expect(result).toEqual(newValue); }); }); }); @@ -98,11 +109,11 @@ describe("deviceTrustCryptoService", () => { jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse); jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue(); - await deviceTrustCryptoService.trustDeviceIfRequired(); + await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1); expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(false); + expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false); }); it("should not trust device nor reset when getShouldTrustDevice returns false", async () => { @@ -112,7 +123,7 @@ describe("deviceTrustCryptoService", () => { const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice"); const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice"); - await deviceTrustCryptoService.trustDeviceIfRequired(); + await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1); expect(trustDeviceSpy).not.toHaveBeenCalled(); @@ -126,53 +137,140 @@ describe("deviceTrustCryptoService", () => { describe("getDeviceKey", () => { let existingDeviceKey: DeviceKey; - let stateSvcGetDeviceKeySpy: jest.SpyInstance; + let existingDeviceKeyB64: { keyB64: string }; beforeEach(() => { existingDeviceKey = new SymmetricCryptoKey( new Uint8Array(deviceKeyBytesLength) as CsprngArray, ) as DeviceKey; - stateSvcGetDeviceKeySpy = jest.spyOn(stateService, "getDeviceKey"); + existingDeviceKeyB64 = existingDeviceKey.toJSON(); }); - it("returns null when there is not an existing device key", async () => { - stateSvcGetDeviceKeySpy.mockResolvedValue(null); + describe("Secure Storage not supported", () => { + it("returns null when there is not an existing device key", async () => { + await stateProvider.setUserState(DEVICE_KEY, null, mockUserId); - const deviceKey = await deviceTrustCryptoService.getDeviceKey(); + const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); - expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1); + expect(deviceKey).toBeNull(); + expect(secureStorageService.get).not.toHaveBeenCalled(); + }); - expect(deviceKey).toBeNull(); + it("returns the device key when there is an existing device key", async () => { + await stateProvider.setUserState(DEVICE_KEY, existingDeviceKey, mockUserId); + + const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + + expect(deviceKey).not.toBeNull(); + expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey); + expect(deviceKey).toEqual(existingDeviceKey); + expect(secureStorageService.get).not.toHaveBeenCalled(); + }); }); - it("returns the device key when there is an existing device key", async () => { - stateSvcGetDeviceKeySpy.mockResolvedValue(existingDeviceKey); + describe("Secure Storage supported", () => { + beforeEach(() => { + const supportsSecureStorage = true; + deviceTrustCryptoService = createDeviceTrustCryptoService( + mockUserId, + supportsSecureStorage, + ); + }); - const deviceKey = await deviceTrustCryptoService.getDeviceKey(); + it("returns null when there is not an existing device key for the passed in user id", async () => { + secureStorageService.get.mockResolvedValue(null); - expect(stateSvcGetDeviceKeySpy).toHaveBeenCalledTimes(1); + // Act + const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); - expect(deviceKey).not.toBeNull(); - expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey); - expect(deviceKey).toEqual(existingDeviceKey); + // Assert + expect(deviceKey).toBeNull(); + }); + + it("returns the device key when there is an existing device key for the passed in user id", async () => { + // Arrange + secureStorageService.get.mockResolvedValue(existingDeviceKeyB64); + + // Act + const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + + // Assert + expect(deviceKey).not.toBeNull(); + expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey); + expect(deviceKey).toEqual(existingDeviceKey); + }); + }); + + it("throws an error when no user id is passed in", async () => { + await expect(deviceTrustCryptoService.getDeviceKey(null)).rejects.toThrow( + "UserId is required. Cannot get device key.", + ); }); }); describe("setDeviceKey", () => { - it("sets the device key in the state service", async () => { - const stateSvcSetDeviceKeySpy = jest.spyOn(stateService, "setDeviceKey"); + describe("Secure Storage not supported", () => { + it("successfully sets the device key in state provider", async () => { + await stateProvider.setUserState(DEVICE_KEY, null, mockUserId); - const deviceKey = new SymmetricCryptoKey( + const newDeviceKey = new SymmetricCryptoKey( + new Uint8Array(deviceKeyBytesLength) as CsprngArray, + ) as DeviceKey; + + // TypeScript will allow calling private methods if the object is of type 'any' + // This is a hacky workaround, but it allows for cleaner tests + await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey); + + expect(stateProvider.mock.setUserState).toHaveBeenLastCalledWith( + DEVICE_KEY, + newDeviceKey.toJSON(), + mockUserId, + ); + }); + }); + describe("Secure Storage supported", () => { + beforeEach(() => { + const supportsSecureStorage = true; + deviceTrustCryptoService = createDeviceTrustCryptoService( + mockUserId, + supportsSecureStorage, + ); + }); + + it("successfully sets the device key in secure storage", async () => { + // Arrange + await stateProvider.setUserState(DEVICE_KEY, null, mockUserId); + + secureStorageService.get.mockResolvedValue(null); + + const newDeviceKey = new SymmetricCryptoKey( + new Uint8Array(deviceKeyBytesLength) as CsprngArray, + ) as DeviceKey; + + // Act + // TypeScript will allow calling private methods if the object is of type 'any' + // This is a hacky workaround, but it allows for cleaner tests + await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey); + + // Assert + expect(stateProvider.mock.setUserState).not.toHaveBeenCalledTimes(2); + expect(secureStorageService.save).toHaveBeenCalledWith( + deviceKeySecureStorageKey, + newDeviceKey, + secureStorageOptions, + ); + }); + }); + + it("throws an error when a null user id is passed in", async () => { + const newDeviceKey = new SymmetricCryptoKey( new Uint8Array(deviceKeyBytesLength) as CsprngArray, ) as DeviceKey; - // TypeScript will allow calling private methods if the object is of type 'any' - // This is a hacky workaround, but it allows for cleaner tests - await (deviceTrustCryptoService as any).setDeviceKey(deviceKey); - - expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledTimes(1); - expect(stateSvcSetDeviceKeySpy).toHaveBeenCalledWith(deviceKey); + await expect( + (deviceTrustCryptoService as any).setDeviceKey(null, newDeviceKey), + ).rejects.toThrow("UserId is required. Cannot set device key."); }); }); @@ -300,7 +398,7 @@ describe("deviceTrustCryptoService", () => { }); it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => { - const response = await deviceTrustCryptoService.trustDevice(); + const response = await deviceTrustCryptoService.trustDevice(mockUserId); expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1); expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1); @@ -331,7 +429,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return null cryptoSvcGetUserKeySpy.mockResolvedValue(null); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow( + await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); @@ -341,7 +439,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return undefined cryptoSvcGetUserKeySpy.mockResolvedValue(undefined); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow( + await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); }); @@ -381,7 +479,9 @@ describe("deviceTrustCryptoService", () => { it(`throws an error if ${method} fails`, async () => { const methodSpy = spy(); methodSpy.mockRejectedValue(new Error(errorText)); - await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(errorText); + await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( + errorText, + ); }); test.each([null, undefined])( @@ -389,11 +489,17 @@ describe("deviceTrustCryptoService", () => { async (invalidValue) => { const methodSpy = spy(); methodSpy.mockResolvedValue(invalidValue); - await expect(deviceTrustCryptoService.trustDevice()).rejects.toThrow(); + await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow(); }, ); }, ); + + it("throws an error when a null user id is passed in", async () => { + await expect(deviceTrustCryptoService.trustDevice(null)).rejects.toThrow( + "UserId is required. Cannot trust device.", + ); + }); }); describe("decryptUserKeyWithDeviceKey", () => { @@ -422,19 +528,26 @@ describe("deviceTrustCryptoService", () => { jest.clearAllMocks(); }); - it("returns null when device key isn't provided and isn't in state", async () => { - const getDeviceKeySpy = jest - .spyOn(deviceTrustCryptoService, "getDeviceKey") - .mockResolvedValue(null); + it("throws an error when a null user id is passed in", async () => { + await expect( + deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + null, + mockEncryptedDevicePrivateKey, + mockEncryptedUserKey, + mockDeviceKey, + ), + ).rejects.toThrow("UserId is required. Cannot decrypt user key with device key."); + }); + it("returns null when device key isn't provided", async () => { const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, + mockDeviceKey, ); expect(result).toBeNull(); - - expect(getDeviceKeySpy).toHaveBeenCalledTimes(1); }); it("successfully returns the user key when provided keys (including device key) can decrypt it", async () => { @@ -446,6 +559,7 @@ describe("deviceTrustCryptoService", () => { .mockResolvedValue(new Uint8Array(userKeyBytesLength)); const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, mockDeviceKey, @@ -456,31 +570,6 @@ describe("deviceTrustCryptoService", () => { expect(rsaDecryptSpy).toHaveBeenCalledTimes(1); }); - it("successfully returns the user key when a device key is not provided (retrieves device key from state)", async () => { - const getDeviceKeySpy = jest - .spyOn(deviceTrustCryptoService, "getDeviceKey") - .mockResolvedValue(mockDeviceKey); - - const decryptToBytesSpy = jest - .spyOn(encryptService, "decryptToBytes") - .mockResolvedValue(new Uint8Array(userKeyBytesLength)); - const rsaDecryptSpy = jest - .spyOn(cryptoService, "rsaDecrypt") - .mockResolvedValue(new Uint8Array(userKeyBytesLength)); - - // Call without providing a device key - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( - mockEncryptedDevicePrivateKey, - mockEncryptedUserKey, - ); - - expect(getDeviceKeySpy).toHaveBeenCalledTimes(1); - - expect(result).toEqual(mockUserKey); - expect(decryptToBytesSpy).toHaveBeenCalledTimes(1); - expect(rsaDecryptSpy).toHaveBeenCalledTimes(1); - }); - it("returns null and removes device key when the decryption fails", async () => { const decryptToBytesSpy = jest .spyOn(encryptService, "decryptToBytes") @@ -488,6 +577,7 @@ describe("deviceTrustCryptoService", () => { const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey"); const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, mockDeviceKey, @@ -496,7 +586,7 @@ describe("deviceTrustCryptoService", () => { expect(result).toBeNull(); expect(decryptToBytesSpy).toHaveBeenCalledTimes(1); expect(setDeviceKeySpy).toHaveBeenCalledTimes(1); - expect(setDeviceKeySpy).toHaveBeenCalledWith(null); + expect(setDeviceKeySpy).toHaveBeenCalledWith(mockUserId, null); }); }); @@ -514,19 +604,28 @@ describe("deviceTrustCryptoService", () => { cryptoService.activeUserKey$ = of(fakeNewUserKey); }); - it("does an early exit when the current device is not a trusted device", async () => { - stateService.getDeviceKey.mockResolvedValue(null); + it("throws an error when a null user id is passed in", async () => { + await expect( + deviceTrustCryptoService.rotateDevicesTrust(null, fakeNewUserKey, ""), + ).rejects.toThrow("UserId is required. Cannot rotate device's trust."); + }); - await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, ""); + it("does an early exit when the current device is not a trusted device", async () => { + const deviceKeyState: FakeActiveUserState = + stateProvider.activeUser.getFake(DEVICE_KEY); + deviceKeyState.nextState(null); + + await deviceTrustCryptoService.rotateDevicesTrust(mockUserId, fakeNewUserKey, ""); expect(devicesApiService.updateTrust).not.toHaveBeenCalled(); }); describe("is on a trusted device", () => { - beforeEach(() => { - stateService.getDeviceKey.mockResolvedValue( - new SymmetricCryptoKey(new Uint8Array(deviceKeyBytesLength)) as DeviceKey, - ); + beforeEach(async () => { + const mockDeviceKey = new SymmetricCryptoKey( + new Uint8Array(deviceKeyBytesLength), + ) as DeviceKey; + await stateProvider.setUserState(DEVICE_KEY, mockDeviceKey, mockUserId); }); it("rotates current device keys and calls api service when the current device is trusted", async () => { @@ -592,7 +691,11 @@ describe("deviceTrustCryptoService", () => { ); }); - await deviceTrustCryptoService.rotateDevicesTrust(fakeNewUserKey, "my_password_hash"); + await deviceTrustCryptoService.rotateDevicesTrust( + mockUserId, + fakeNewUserKey, + "my_password_hash", + ); expect(devicesApiService.updateTrust).toHaveBeenCalledWith( matches((updateTrustModel: UpdateDevicesTrustRequest) => { @@ -608,4 +711,32 @@ describe("deviceTrustCryptoService", () => { }); }); }); + + // Helpers + function createDeviceTrustCryptoService( + mockUserId: UserId | null, + supportsSecureStorage: boolean, + ) { + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + platformUtilsService.supportsSecureStorage.mockReturnValue(supportsSecureStorage); + + decryptionOptions.next({} as any); + userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; + + return new DeviceTrustCryptoService( + keyGenerationService, + cryptoFunctionService, + cryptoService, + encryptService, + appIdService, + devicesApiService, + i18nService, + platformUtilsService, + stateProvider, + secureStorageService, + userDecryptionOptionsService, + ); + } }); diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts new file mode 100644 index 0000000000..e3e5fbdbe7 --- /dev/null +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -0,0 +1,382 @@ +import { mock } from "jest-mock-extended"; + +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; +import { ApiService } from "../../abstractions/api.service"; +import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationData } from "../../admin-console/models/data/organization.data"; +import { Organization } from "../../admin-console/models/domain/organization"; +import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { LogService } from "../../platform/abstractions/log.service"; +import { Utils } from "../../platform/misc/utils"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { KeyGenerationService } from "../../platform/services/key-generation.service"; +import { OrganizationId, UserId } from "../../types/guid"; +import { MasterKey } from "../../types/key"; +import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; +import { KeyConnectorUserKeyResponse } from "../models/response/key-connector-user-key.response"; + +import { + USES_KEY_CONNECTOR, + CONVERT_ACCOUNT_TO_KEY_CONNECTOR, + KeyConnectorService, +} from "./key-connector.service"; +import { FakeMasterPasswordService } from "./master-password/fake-master-password.service"; +import { TokenService } from "./token.service"; + +describe("KeyConnectorService", () => { + let keyConnectorService: KeyConnectorService; + + const cryptoService = mock(); + const apiService = mock(); + const tokenService = mock(); + const logService = mock(); + const organizationService = mock(); + const keyGenerationService = mock(); + + let stateProvider: FakeStateProvider; + + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; + + const mockUserId = Utils.newGuid() as UserId; + const mockOrgId = Utils.newGuid() as OrganizationId; + + const mockMasterKeyResponse: KeyConnectorUserKeyResponse = new KeyConnectorUserKeyResponse({ + key: "eO9nVlVl3I3sU6O+CyK0kEkpGtl/auT84Hig2WTXmZtDTqYtKpDvUPfjhgMOHf+KQzx++TVS2AOLYq856Caa7w==", + }); + + beforeEach(() => { + jest.clearAllMocks(); + + masterPasswordService = new FakeMasterPasswordService(); + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + keyConnectorService = new KeyConnectorService( + accountService, + masterPasswordService, + cryptoService, + apiService, + tokenService, + logService, + organizationService, + keyGenerationService, + async () => {}, + stateProvider, + ); + }); + + it("instantiates", () => { + expect(keyConnectorService).not.toBeFalsy(); + }); + + describe("setUsesKeyConnector()", () => { + it("should update the usesKeyConnectorState with the provided value", async () => { + const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); + state.nextState(false); + + const newValue = true; + + await keyConnectorService.setUsesKeyConnector(newValue); + + expect(await keyConnectorService.getUsesKeyConnector()).toBe(newValue); + }); + }); + + describe("getManagingOrganization()", () => { + it("should return the managing organization with key connector enabled", async () => { + // Arrange + const orgs = [ + organizationData(true, true, "https://key-connector-url.com", 2, false), + organizationData(false, true, "https://key-connector-url.com", 2, false), + organizationData(true, false, "https://key-connector-url.com", 2, false), + organizationData(true, true, "https://other-url.com", 2, false), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toEqual(orgs[0]); + }); + + it("should return undefined if no managing organization with key connector enabled is found", async () => { + // Arrange + const orgs = [ + organizationData(true, false, "https://key-connector-url.com", 2, false), + organizationData(false, false, "https://key-connector-url.com", 2, false), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toBeUndefined(); + }); + + it("should return undefined if user is Owner or Admin", async () => { + // Arrange + const orgs = [ + organizationData(true, true, "https://key-connector-url.com", 0, false), + organizationData(true, true, "https://key-connector-url.com", 1, false), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toBeUndefined(); + }); + + it("should return undefined if user is a Provider", async () => { + // Arrange + const orgs = [ + organizationData(true, true, "https://key-connector-url.com", 2, true), + organizationData(false, true, "https://key-connector-url.com", 2, true), + ]; + organizationService.getAll.mockResolvedValue(orgs); + + // Act + const result = await keyConnectorService.getManagingOrganization(); + + // Assert + expect(result).toBeUndefined(); + }); + }); + + describe("setConvertAccountRequired()", () => { + it("should update the convertAccountToKeyConnectorState with the provided value", async () => { + const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR); + state.nextState(false); + + const newValue = true; + + await keyConnectorService.setConvertAccountRequired(newValue); + + expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue); + }); + + it("should remove the convertAccountToKeyConnectorState", async () => { + const state = stateProvider.activeUser.getFake(CONVERT_ACCOUNT_TO_KEY_CONNECTOR); + state.nextState(false); + + const newValue: boolean = null; + + await keyConnectorService.setConvertAccountRequired(newValue); + + expect(await keyConnectorService.getConvertAccountRequired()).toBe(newValue); + }); + }); + + describe("userNeedsMigration()", () => { + it("should return true if the user needs migration", async () => { + // token + tokenService.getIsExternal.mockResolvedValue(true); + + // create organization object + const data = organizationData(true, true, "https://key-connector-url.com", 2, false); + organizationService.getAll.mockResolvedValue([data]); + + // uses KeyConnector + const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); + state.nextState(false); + + const result = await keyConnectorService.userNeedsMigration(); + + expect(result).toBe(true); + }); + + it("should return false if the user does not need migration", async () => { + tokenService.getIsExternal.mockResolvedValue(false); + const data = organizationData(false, false, "https://key-connector-url.com", 2, false); + organizationService.getAll.mockResolvedValue([data]); + + const state = stateProvider.activeUser.getFake(USES_KEY_CONNECTOR); + state.nextState(true); + const result = await keyConnectorService.userNeedsMigration(); + + expect(result).toBe(false); + }); + }); + + describe("setMasterKeyFromUrl", () => { + it("should set the master key from the provided URL", async () => { + // Arrange + const url = "https://key-connector-url.com"; + + apiService.getMasterKeyFromKeyConnector.mockResolvedValue(mockMasterKeyResponse); + + // Hard to mock these, but we can generate the same keys + const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key); + const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; + + // Act + await keyConnectorService.setMasterKeyFromUrl(url); + + // Assert + expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); + expect(masterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( + masterKey, + expect.any(String), + ); + }); + + it("should handle errors thrown during the process", async () => { + // Arrange + const url = "https://key-connector-url.com"; + + const error = new Error("Failed to get master key"); + apiService.getMasterKeyFromKeyConnector.mockRejectedValue(error); + jest.spyOn(logService, "error"); + + try { + // Act + await keyConnectorService.setMasterKeyFromUrl(url); + } catch { + // Assert + expect(logService.error).toHaveBeenCalledWith(error); + expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); + } + }); + }); + + describe("migrateUser()", () => { + it("should migrate the user to the key connector", async () => { + // Arrange + const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); + const masterKey = getMockMasterKey(); + masterPasswordService.masterKeySubject.next(masterKey); + const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + + jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); + jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue(); + + // Act + await keyConnectorService.migrateUser(); + + // Assert + expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); + expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( + organization.keyConnectorUrl, + keyConnectorRequest, + ); + expect(apiService.postConvertToKeyConnector).toHaveBeenCalled(); + }); + + it("should handle errors thrown during migration", async () => { + // Arrange + const organization = organizationData(true, true, "https://key-connector-url.com", 2, false); + const masterKey = getMockMasterKey(); + const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); + const error = new Error("Failed to post user key to key connector"); + organizationService.getAll.mockResolvedValue([organization]); + + masterPasswordService.masterKeySubject.next(masterKey); + jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization); + jest.spyOn(apiService, "postUserKeyToKeyConnector").mockRejectedValue(error); + jest.spyOn(logService, "error"); + + try { + // Act + await keyConnectorService.migrateUser(); + } catch { + // Assert + expect(logService.error).toHaveBeenCalledWith(error); + expect(keyConnectorService.getManagingOrganization).toHaveBeenCalled(); + expect(apiService.postUserKeyToKeyConnector).toHaveBeenCalledWith( + organization.keyConnectorUrl, + keyConnectorRequest, + ); + } + }); + }); + + function organizationData( + usesKeyConnector: boolean, + keyConnectorEnabled: boolean, + keyConnectorUrl: string, + userType: number, + isProviderUser: boolean, + ): Organization { + return new Organization( + new OrganizationData( + new ProfileOrganizationResponse({ + id: mockOrgId, + name: "TEST_KEY_CONNECTOR_ORG", + usePolicies: true, + useSso: true, + useKeyConnector: usesKeyConnector, + useScim: true, + useGroups: true, + useDirectory: true, + useEvents: true, + useTotp: true, + use2fa: true, + useApi: true, + useResetPassword: true, + useSecretsManager: true, + usePasswordManager: true, + usersGetPremium: true, + useCustomPermissions: true, + useActivateAutofillPolicy: true, + selfHost: true, + seats: 5, + maxCollections: null, + maxStorageGb: 1, + key: "super-secret-key", + status: 2, + type: userType, + enabled: true, + ssoBound: true, + identifier: "TEST_KEY_CONNECTOR_ORG", + permissions: { + accessEventLogs: false, + accessImportExport: false, + accessReports: false, + createNewCollections: false, + editAnyCollection: false, + deleteAnyCollection: false, + editAssignedCollections: false, + deleteAssignedCollections: false, + manageGroups: false, + managePolicies: false, + manageSso: false, + manageUsers: false, + manageResetPassword: false, + manageScim: false, + }, + resetPasswordEnrolled: true, + userId: mockUserId, + hasPublicAndPrivateKeys: true, + providerId: null, + providerName: null, + providerType: null, + familySponsorshipFriendlyName: null, + familySponsorshipAvailable: true, + planProductType: 3, + KeyConnectorEnabled: keyConnectorEnabled, + KeyConnectorUrl: keyConnectorUrl, + familySponsorshipLastSyncDate: null, + familySponsorshipValidUntil: null, + familySponsorshipToDelete: null, + accessSecretsManager: false, + limitCollectionCreationDeletion: true, + allowAdminAccessToAllCollectionItems: true, + flexibleCollections: false, + object: "profileOrganization", + }), + { isMember: true, isProviderUser: isProviderUser }, + ), + ); + } + + function getMockMasterKey(): MasterKey { + const keyArr = Utils.fromB64ToArray(mockMasterKeyResponse.key); + const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; + return masterKey; + } +}); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index cded13a74b..f8e523cce4 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -1,3 +1,5 @@ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "../../abstractions/api.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; @@ -5,20 +7,48 @@ import { KeysRequest } from "../../models/request/keys.request"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; -import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { + ActiveUserState, + KEY_CONNECTOR_DISK, + StateProvider, + UserKeyDefinition, +} from "../../platform/state"; import { MasterKey } from "../../types/key"; +import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; import { KdfConfig } from "../models/domain/kdf-config"; import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request"; import { IdentityTokenResponse } from "../models/response/identity-token.response"; +export const USES_KEY_CONNECTOR = new UserKeyDefinition( + KEY_CONNECTOR_DISK, + "usesKeyConnector", + { + deserializer: (usesKeyConnector) => usesKeyConnector, + clearOn: ["logout"], + }, +); + +export const CONVERT_ACCOUNT_TO_KEY_CONNECTOR = new UserKeyDefinition( + KEY_CONNECTOR_DISK, + "convertAccountToKeyConnector", + { + deserializer: (convertAccountToKeyConnector) => convertAccountToKeyConnector, + clearOn: ["logout"], + }, +); + export class KeyConnectorService implements KeyConnectorServiceAbstraction { + private usesKeyConnectorState: ActiveUserState; + private convertAccountToKeyConnectorState: ActiveUserState; constructor( - private stateService: StateService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private apiService: ApiService, private tokenService: TokenService, @@ -26,14 +56,20 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { private organizationService: OrganizationService, private keyGenerationService: KeyGenerationService, private logoutCallback: (expired: boolean, userId?: string) => Promise, - ) {} - - setUsesKeyConnector(usesKeyConnector: boolean) { - return this.stateService.setUsesKeyConnector(usesKeyConnector); + private stateProvider: StateProvider, + ) { + this.usesKeyConnectorState = this.stateProvider.getActive(USES_KEY_CONNECTOR); + this.convertAccountToKeyConnectorState = this.stateProvider.getActive( + CONVERT_ACCOUNT_TO_KEY_CONNECTOR, + ); } - async getUsesKeyConnector(): Promise { - return await this.stateService.getUsesKeyConnector(); + async setUsesKeyConnector(usesKeyConnector: boolean) { + await this.usesKeyConnectorState.update(() => usesKeyConnector); + } + + getUsesKeyConnector(): Promise { + return firstValueFrom(this.usesKeyConnectorState.state$); } async userNeedsMigration() { @@ -46,7 +82,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { async migrateUser() { const organization = await this.getManagingOrganization(); - const masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); try { @@ -67,7 +104,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url); const keyArr = Utils.fromB64ToArray(masterKeyResponse.key); const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); } catch (e) { this.handleKeyConnectorError(e); } @@ -104,7 +142,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); - await this.cryptoService.setMasterKey(masterKey); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.cryptoService.makeUserKey(masterKey); await this.cryptoService.setUserKey(userKey[0]); @@ -132,19 +171,15 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } async setConvertAccountRequired(status: boolean) { - await this.stateService.setConvertAccountToKeyConnector(status); + await this.convertAccountToKeyConnectorState.update(() => status); } - async getConvertAccountRequired(): Promise { - return await this.stateService.getConvertAccountToKeyConnector(); + getConvertAccountRequired(): Promise { + return firstValueFrom(this.convertAccountToKeyConnectorState.state$); } async removeConvertAccountRequired() { - await this.stateService.setConvertAccountToKeyConnector(null); - } - - async clear() { - await this.removeConvertAccountRequired(); + await this.setConvertAccountRequired(null); } private handleKeyConnectorError(e: any) { diff --git a/libs/common/src/auth/services/login.service.ts b/libs/common/src/auth/services/login.service.ts deleted file mode 100644 index f1d038b2f8..0000000000 --- a/libs/common/src/auth/services/login.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { StateService } from "../../platform/abstractions/state.service"; -import { LoginService as LoginServiceAbstraction } from "../abstractions/login.service"; - -export class LoginService implements LoginServiceAbstraction { - private _email: string; - private _rememberEmail: boolean; - - constructor(private stateService: StateService) {} - - getEmail() { - return this._email; - } - - getRememberEmail() { - return this._rememberEmail; - } - - setEmail(value: string) { - this._email = value; - } - - setRememberEmail(value: boolean) { - this._rememberEmail = value; - } - - clearValues() { - this._email = null; - this._rememberEmail = null; - } - - async saveEmailSettings() { - await this.stateService.setRememberedEmail(this._rememberEmail ? this._email : null); - this.clearValues(); - } -} diff --git a/libs/common/src/auth/services/master-password/fake-master-password.service.ts b/libs/common/src/auth/services/master-password/fake-master-password.service.ts new file mode 100644 index 0000000000..dd034ec50b --- /dev/null +++ b/libs/common/src/auth/services/master-password/fake-master-password.service.ts @@ -0,0 +1,64 @@ +import { mock } from "jest-mock-extended"; +import { ReplaySubject, Observable } from "rxjs"; + +import { EncString } from "../../../platform/models/domain/enc-string"; +import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; +import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; + +export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction { + mock = mock(); + + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeySubject = new ReplaySubject(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + masterKeyHashSubject = new ReplaySubject(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + forceSetPasswordReasonSubject = new ReplaySubject(1); + + constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) { + this.masterKeySubject.next(initialMasterKey); + this.masterKeyHashSubject.next(initialMasterKeyHash); + } + + masterKey$(userId: UserId): Observable { + return this.masterKeySubject.asObservable(); + } + + setMasterKey(masterKey: MasterKey, userId: UserId): Promise { + return this.mock.setMasterKey(masterKey, userId); + } + + clearMasterKey(userId: UserId): Promise { + return this.mock.clearMasterKey(userId); + } + + masterKeyHash$(userId: UserId): Observable { + return this.masterKeyHashSubject.asObservable(); + } + + getMasterKeyEncryptedUserKey(userId: UserId): Promise { + return this.mock.getMasterKeyEncryptedUserKey(userId); + } + + setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise { + return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId); + } + + setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise { + return this.mock.setMasterKeyHash(masterKeyHash, userId); + } + + clearMasterKeyHash(userId: UserId): Promise { + return this.mock.clearMasterKeyHash(userId); + } + + forceSetPasswordReason$(userId: UserId): Observable { + return this.forceSetPasswordReasonSubject.asObservable(); + } + + setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise { + return this.mock.setForceSetPasswordReason(reason, userId); + } +} diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts new file mode 100644 index 0000000000..fad48abc12 --- /dev/null +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -0,0 +1,140 @@ +import { firstValueFrom, map, Observable } from "rxjs"; + +import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { + MASTER_PASSWORD_DISK, + MASTER_PASSWORD_MEMORY, + StateProvider, + UserKeyDefinition, +} from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { MasterKey } from "../../../types/key"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; +import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; + +/** Memory since master key shouldn't be available on lock */ +const MASTER_KEY = new UserKeyDefinition(MASTER_PASSWORD_MEMORY, "masterKey", { + deserializer: (masterKey) => SymmetricCryptoKey.fromJSON(masterKey) as MasterKey, + clearOn: ["lock", "logout"], +}); + +/** Disk since master key hash is used for unlock */ +const MASTER_KEY_HASH = new UserKeyDefinition(MASTER_PASSWORD_DISK, "masterKeyHash", { + deserializer: (masterKeyHash) => masterKeyHash, + clearOn: ["logout"], +}); + +/** Disk to persist through lock */ +const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition( + MASTER_PASSWORD_DISK, + "masterKeyEncryptedUserKey", + { + deserializer: (key) => key, + clearOn: ["logout"], + }, +); + +/** Disk to persist through lock and account switches */ +const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition( + MASTER_PASSWORD_DISK, + "forceSetPasswordReason", + { + deserializer: (reason) => reason, + clearOn: ["logout"], + }, +); + +export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + + masterKey$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY).state$; + } + + masterKeyHash$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$; + } + + forceSetPasswordReason$(userId: UserId): Observable { + if (userId == null) { + throw new Error("User ID is required."); + } + return this.stateProvider + .getUser(userId, FORCE_SET_PASSWORD_REASON) + .state$.pipe(map((reason) => reason ?? ForceSetPasswordReason.None)); + } + + // TODO: Remove this method and decrypt directly in the service instead + async getMasterKeyEncryptedUserKey(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + const key = await firstValueFrom( + this.stateProvider.getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY).state$, + ); + return EncString.fromJSON(key); + } + + async setMasterKey(masterKey: MasterKey, userId: UserId): Promise { + if (masterKey == null) { + throw new Error("Master key is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => masterKey); + } + + async clearMasterKey(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => null); + } + + async setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise { + if (masterKeyHash == null) { + throw new Error("Master key hash is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => masterKeyHash); + } + + async clearMasterKeyHash(userId: UserId): Promise { + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => null); + } + + async setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise { + if (encryptedKey == null) { + throw new Error("Encrypted Key is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider + .getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY) + .update((_) => encryptedKey.toJSON() as EncryptedString); + } + + async setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise { + if (reason == null) { + throw new Error("Reason is required."); + } + if (userId == null) { + throw new Error("User ID is required."); + } + await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason); + } +} diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index 408ed33c97..fc5060af5f 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -8,7 +8,6 @@ import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models import { CryptoService } from "../../platform/abstractions/crypto.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { AccountInfo, AccountService } from "../abstractions/account.service"; -import { AuthenticationStatus } from "../enums/authentication-status"; import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation"; @@ -91,7 +90,6 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { const user1AccountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", - status: AuthenticationStatus.Unlocked, }; activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); diff --git a/libs/common/src/auth/services/sso-login.service.ts b/libs/common/src/auth/services/sso-login.service.ts index 99640e1c6c..3df6ef3540 100644 --- a/libs/common/src/auth/services/sso-login.service.ts +++ b/libs/common/src/auth/services/sso-login.service.ts @@ -6,6 +6,7 @@ import { KeyDefinition, SSO_DISK, StateProvider, + UserKeyDefinition, } from "../../platform/state"; import { SsoLoginServiceAbstraction } from "../abstractions/sso-login.service.abstraction"; @@ -26,7 +27,19 @@ const SSO_STATE = new KeyDefinition(SSO_DISK, "ssoState", { /** * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. */ -const ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( +const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition( + SSO_DISK, + "organizationSsoIdentifier", + { + deserializer: (organizationIdentifier) => organizationIdentifier, + clearOn: ["logout"], // Used for login, so not needed past logout + }, +); + +/** + * Uses disk storage so that the organization sso identifier can be persisted across sso redirects. + */ +const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition( SSO_DISK, "organizationSsoIdentifier", { @@ -51,10 +64,10 @@ export class SsoLoginService implements SsoLoginServiceAbstraction { constructor(private stateProvider: StateProvider) { this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER); this.ssoState = this.stateProvider.getGlobal(SSO_STATE); - this.orgSsoIdentifierState = this.stateProvider.getGlobal(ORGANIZATION_SSO_IDENTIFIER); + this.orgSsoIdentifierState = this.stateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER); this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL); this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive( - ORGANIZATION_SSO_IDENTIFIER, + USER_ORGANIZATION_SSO_IDENTIFIER, ); } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index a7b953f928..d32c4d8e1c 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -1,7 +1,11 @@ -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; import { StorageOptions } from "../../platform/models/domain/storage-options"; @@ -12,7 +16,6 @@ import { DecodedAccessToken, TokenService } from "./token.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, API_KEY_CLIENT_ID_DISK, API_KEY_CLIENT_ID_MEMORY, API_KEY_CLIENT_SECRET_DISK, @@ -20,7 +23,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, } from "./token.state"; describe("TokenService", () => { @@ -28,7 +30,10 @@ describe("TokenService", () => { let singleUserStateProvider: FakeSingleUserStateProvider; let globalStateProvider: FakeGlobalStateProvider; - const secureStorageService = mock(); + let secureStorageService: MockProxy; + let keyGenerationService: MockProxy; + let encryptService: MockProxy; + let logService: MockProxy; const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut; const memoryVaultTimeout = 30; @@ -74,12 +79,19 @@ describe("TokenService", () => { userId: userIdFromAccessToken, }; + const accessTokenKeyB64 = { keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8" }; + beforeEach(() => { jest.clearAllMocks(); singleUserStateProvider = new FakeSingleUserStateProvider(); globalStateProvider = new FakeGlobalStateProvider(); + secureStorageService = mock(); + keyGenerationService = mock(); + encryptService = mock(); + logService = mock(); + const supportsSecureStorage = false; // default to false; tests will override as needed tokenService = createTokenService(supportsSecureStorage); }); @@ -89,8 +101,63 @@ describe("TokenService", () => { }); describe("Access Token methods", () => { - const accessTokenPartialSecureStorageKey = `_accessToken`; - const accessTokenSecureStorageKey = `${userIdFromAccessToken}${accessTokenPartialSecureStorageKey}`; + const accessTokenKeyPartialSecureStorageKey = `_accessTokenKey`; + const accessTokenKeySecureStorageKey = `${userIdFromAccessToken}${accessTokenKeyPartialSecureStorageKey}`; + + describe("hasAccessToken$", () => { + it("returns true when an access token exists in memory", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + + // Act + const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken)); + + // Assert + expect(result).toEqual(true); + }); + + it("returns true when an access token exists in disk", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) + .stateSubject.next([userIdFromAccessToken, undefined]); + + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + + // Act + const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken)); + + // Assert + expect(result).toEqual(true); + }); + + it("returns true when an access token exists in secure storage", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) + .stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]); + + secureStorageService.get.mockResolvedValue(accessTokenKeyB64); + + // Act + const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken)); + + // Assert + expect(result).toEqual(true); + }); + + it("should return false if no access token exists in memory, disk, or secure storage", async () => { + // Act + const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken)); + + // Assert + expect(result).toEqual(false); + }); + }); describe("setAccessToken", () => { it("should throw an error if the access token is null", async () => { @@ -150,18 +217,22 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should set the access token in secure storage, null out data on disk or in memory, and set a flag to indicate the token has been migrated", async () => { + it("should set an access token key in secure storage, the encrypted access token in disk, and clear out the token in memory", async () => { // Arrange: - // For testing purposes, let's assume that the access token is already in disk and memory - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - + // For testing purposes, let's assume that the access token is already in memory singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + keyGenerationService.createKey.mockResolvedValue("accessTokenKey" as any); + + const mockEncryptedAccessToken = "encryptedAccessToken"; + + encryptService.encrypt.mockResolvedValue({ + encryptedString: mockEncryptedAccessToken, + } as any); + // Act await tokenService.setAccessToken( accessTokenJwt, @@ -170,27 +241,22 @@ describe("TokenService", () => { ); // Assert - // assert that the access token was set in secure storage + // assert that the AccessTokenKey was set in secure storage expect(secureStorageService.save).toHaveBeenCalledWith( - accessTokenSecureStorageKey, - accessTokenJwt, + accessTokenKeySecureStorageKey, + "accessTokenKey", secureStorageOptions, ); - // assert data was migrated out of disk and memory + flag was set + // assert that the access token was encrypted and set in disk expect( singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock, - ).toHaveBeenCalledWith(null); + ).toHaveBeenCalledWith(mockEncryptedAccessToken); + + // assert data was migrated out of memory expect( singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock, ).toHaveBeenCalledWith(null); - - expect( - singleUserStateProvider.getFake( - userIdFromAccessToken, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, - ).nextMock, - ).toHaveBeenCalledWith(true); }); }); }); @@ -216,7 +282,13 @@ describe("TokenService", () => { }); describe("Memory storage tests", () => { - it("should get the access token from memory with no user id specified (uses global active user)", async () => { + test.each([ + [ + "should get the access token from memory for the provided user id", + userIdFromAccessToken, + ], + ["should get the access token from memory with no user id provided", undefined], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -228,37 +300,28 @@ describe("TokenService", () => { .stateSubject.next([userIdFromAccessToken, undefined]); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } // Act - const result = await tokenService.getAccessToken(); + const result = await tokenService.getAccessToken(userId); // Assert expect(result).toEqual(accessTokenJwt); }); - - it("should get the access token from memory for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - // set disk to undefined - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); - - // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); - // Assert - expect(result).toEqual(accessTokenJwt); - }); }); describe("Disk storage tests (secure storage not supported on platform)", () => { - it("should get the access token from disk with no user id specified", async () => { + test.each([ + [ + "should get the access token from disk for the specified user id", + userIdFromAccessToken, + ], + ["should get the access token from disk with no user id specified", undefined], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -269,28 +332,14 @@ describe("TokenService", () => { .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } // Act - const result = await tokenService.getAccessToken(); - // Assert - expect(result).toEqual(accessTokenJwt); - }); - - it("should get the access token from disk for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); + const result = await tokenService.getAccessToken(userId); // Assert expect(result).toEqual(accessTokenJwt); }); @@ -302,7 +351,16 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should get the access token from secure storage when no user id is specified and the migration flag is set to true", async () => { + test.each([ + [ + "should get the encrypted access token from disk, decrypt it, and return it when user id is provided", + userIdFromAccessToken, + ], + [ + "should get the encrypted access token from disk, decrypt it, and return it when no user id is provided", + undefined, + ], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -310,76 +368,35 @@ describe("TokenService", () => { singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); + .stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]); - secureStorageService.get.mockResolvedValue(accessTokenJwt); + secureStorageService.get.mockResolvedValue(accessTokenKeyB64); + encryptService.decryptToUtf8.mockResolvedValue("decryptedAccessToken"); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } // Act - const result = await tokenService.getAccessToken(); - // Assert - expect(result).toEqual(accessTokenJwt); - }); - - it("should get the access token from secure storage when user id is specified and the migration flag set to true", async () => { - // Arrange - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, undefined]); - - secureStorageService.get.mockResolvedValue(accessTokenJwt); - - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); - - // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); - // Assert - expect(result).toEqual(accessTokenJwt); - }); - - it("should fallback and get the access token from disk when user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, undefined]); - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - // set access token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); - - // Act - const result = await tokenService.getAccessToken(userIdFromAccessToken); + const result = await tokenService.getAccessToken(userId); // Assert - expect(result).toEqual(accessTokenJwt); - - // assert that secure storage was not called - expect(secureStorageService.get).not.toHaveBeenCalled(); + expect(result).toEqual("decryptedAccessToken"); }); - it("should fallback and get the access token from disk when no user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => { + test.each([ + [ + "should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and a user id is provided", + userIdFromAccessToken, + ], + [ + "should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and no user id is provided", + undefined, + ], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -390,23 +407,19 @@ describe("TokenService", () => { .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } - // set access token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); + // No access token key set // Act - const result = await tokenService.getAccessToken(); + const result = await tokenService.getAccessToken(userId); // Assert expect(result).toEqual(accessTokenJwt); - - // assert that secure storage was not called - expect(secureStorageService.get).not.toHaveBeenCalled(); }); }); }); @@ -426,7 +439,16 @@ describe("TokenService", () => { tokenService = createTokenService(supportsSecureStorage); }); - it("should clear the access token from all storage locations for the specified user id", async () => { + test.each([ + [ + "should clear the access token from all storage locations for the provided user id", + userIdFromAccessToken, + ], + [ + "should clear the access token from all storage locations for the global active user", + undefined, + ], + ])("%s", async (_, userId) => { // Arrange singleUserStateProvider .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) @@ -436,6 +458,13 @@ describe("TokenService", () => { .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); + // Need to have global active id set to the user id + if (!userId) { + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + } + // Act await tokenService.clearAccessToken(userIdFromAccessToken); @@ -448,39 +477,7 @@ describe("TokenService", () => { ).toHaveBeenCalledWith(null); expect(secureStorageService.remove).toHaveBeenCalledWith( - accessTokenSecureStorageKey, - secureStorageOptions, - ); - }); - - it("should clear the access token from all storage locations for the global active user", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - singleUserStateProvider - .getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK) - .stateSubject.next([userIdFromAccessToken, accessTokenJwt]); - - // Need to have global active id set to the user id - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - // Act - await tokenService.clearAccessToken(); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock, - ).toHaveBeenCalledWith(null); - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock, - ).toHaveBeenCalledWith(null); - - expect(secureStorageService.remove).toHaveBeenCalledWith( - accessTokenSecureStorageKey, + accessTokenKeySecureStorageKey, secureStorageOptions, ); }); @@ -1049,6 +1046,7 @@ describe("TokenService", () => { refreshToken, VaultTimeoutAction.Lock, null, + null, ); // Assert await expect(result).rejects.toThrow("User id not found. Cannot save refresh token."); @@ -1121,20 +1119,13 @@ describe("TokenService", () => { secureStorageOptions, ); - // assert data was migrated out of disk and memory + flag was set + // assert data was migrated out of disk and memory expect( singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock, ).toHaveBeenCalledWith(null); expect( singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock, ).toHaveBeenCalledWith(null); - - expect( - singleUserStateProvider.getFake( - userIdFromAccessToken, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, - ).nextMock, - ).toHaveBeenCalledWith(true); }); }); }); @@ -1261,11 +1252,6 @@ describe("TokenService", () => { .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) .stateSubject.next(userIdFromAccessToken); - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); - // Act const result = await tokenService.getRefreshToken(); // Assert @@ -1285,11 +1271,6 @@ describe("TokenService", () => { secureStorageService.get.mockResolvedValue(refreshToken); - // set access token migration flag to true - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, true]); - // Act const result = await tokenService.getRefreshToken(userIdFromAccessToken); // Assert @@ -1306,11 +1287,6 @@ describe("TokenService", () => { .getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK) .stateSubject.next([userIdFromAccessToken, refreshToken]); - // set refresh token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); - // Act const result = await tokenService.getRefreshToken(userIdFromAccessToken); @@ -1336,11 +1312,6 @@ describe("TokenService", () => { .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) .stateSubject.next(userIdFromAccessToken); - // set access token migration flag to false - singleUserStateProvider - .getFake(userIdFromAccessToken, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .stateSubject.next([userIdFromAccessToken, false]); - // Act const result = await tokenService.getRefreshToken(); @@ -1912,7 +1883,7 @@ describe("TokenService", () => { // Act // Note: passing a valid access token so that a valid user id can be determined from the access token - await tokenService.setTokens(accessTokenJwt, refreshToken, vaultTimeoutAction, vaultTimeout, [ + await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken, [ clientId, clientSecret, ]); @@ -1959,7 +1930,7 @@ describe("TokenService", () => { tokenService.setClientSecret = jest.fn(); // Act - await tokenService.setTokens(accessTokenJwt, refreshToken, vaultTimeoutAction, vaultTimeout); + await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken); // Assert expect((tokenService as any)._setAccessToken).toHaveBeenCalledWith( @@ -1991,9 +1962,9 @@ describe("TokenService", () => { // Act const result = tokenService.setTokens( accessToken, - refreshToken, vaultTimeoutAction, vaultTimeout, + refreshToken, ); // Assert @@ -2010,32 +1981,27 @@ describe("TokenService", () => { // Act const result = tokenService.setTokens( accessToken, - refreshToken, vaultTimeoutAction, vaultTimeout, + refreshToken, ); // Assert - await expect(result).rejects.toThrow("Access token and refresh token are required."); + await expect(result).rejects.toThrow("Access token is required."); }); - it("should throw an error if the refresh token is missing", async () => { + it("should not throw an error if the refresh token is missing and it should just not set it", async () => { // Arrange - const accessToken = "accessToken"; const refreshToken: string = null; const vaultTimeoutAction = VaultTimeoutAction.Lock; const vaultTimeout = 30; + (tokenService as any).setRefreshToken = jest.fn(); // Act - const result = tokenService.setTokens( - accessToken, - refreshToken, - vaultTimeoutAction, - vaultTimeout, - ); + await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken); // Assert - await expect(result).rejects.toThrow("Access token and refresh token are required."); + expect((tokenService as any).setRefreshToken).not.toHaveBeenCalled(); }); }); @@ -2232,6 +2198,9 @@ describe("TokenService", () => { globalStateProvider, supportsSecureStorage, secureStorageService, + keyGenerationService, + encryptService, + logService, ); } }); diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 4e9722614e..c24a2c186b 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -1,16 +1,22 @@ -import { firstValueFrom } from "rxjs"; +import { Observable, combineLatest, firstValueFrom, map } from "rxjs"; +import { Opaque } from "type-fest"; import { decodeJwtTokenToJson } from "@bitwarden/auth/common"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; +import { EncString, EncryptedString } from "../../platform/models/domain/enc-string"; import { StorageOptions } from "../../platform/models/domain/storage-options"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { GlobalState, GlobalStateProvider, - KeyDefinition, SingleUserStateProvider, + UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; @@ -19,7 +25,6 @@ import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, API_KEY_CLIENT_ID_DISK, API_KEY_CLIENT_ID_MEMORY, API_KEY_CLIENT_SECRET_DISK, @@ -27,7 +32,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, } from "./token.state"; export enum TokenStorageLocation { @@ -101,8 +105,14 @@ export type DecodedAccessToken = { jti?: string; }; +/** + * A symmetric key for encrypting the access token before the token is stored on disk. + * This key should be stored in secure storage. + * */ +type AccessTokenKey = Opaque; + export class TokenService implements TokenServiceAbstraction { - private readonly accessTokenSecureStorageKey: string = "_accessToken"; + private readonly accessTokenKeySecureStorageKey: string = "_accessTokenKey"; private readonly refreshTokenSecureStorageKey: string = "_refreshToken"; @@ -117,10 +127,26 @@ export class TokenService implements TokenServiceAbstraction { private globalStateProvider: GlobalStateProvider, private readonly platformSupportsSecureStorage: boolean, private secureStorageService: AbstractStorageService, + private keyGenerationService: KeyGenerationService, + private encryptService: EncryptService, + private logService: LogService, ) { this.initializeState(); } + hasAccessToken$(userId: UserId): Observable { + // FIXME Once once vault timeout action is observable, we can use it to determine storage location + // and avoid the need to check both disk and memory. + return combineLatest([ + this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).state$, + this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).state$, + ]).pipe(map(([disk, memory]) => Boolean(disk || memory))); + } + + // pivoting to an approach where we create a symmetric key we store in secure storage + // which is used to protect the data before persisting to disk. + // We will also use the same symmetric key to decrypt the data when reading from disk. + private initializeState(): void { this.emailTwoFactorTokenRecordGlobalState = this.globalStateProvider.get( EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, @@ -131,13 +157,13 @@ export class TokenService implements TokenServiceAbstraction { async setTokens( accessToken: string, - refreshToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, + refreshToken?: string, clientIdClientSecret?: [string, string], ): Promise { - if (!accessToken || !refreshToken) { - throw new Error("Access token and refresh token are required."); + if (!accessToken) { + throw new Error("Access token is required."); } // get user id the access token @@ -148,13 +174,95 @@ export class TokenService implements TokenServiceAbstraction { } await this._setAccessToken(accessToken, vaultTimeoutAction, vaultTimeout, userId); - await this.setRefreshToken(refreshToken, vaultTimeoutAction, vaultTimeout, userId); + + if (refreshToken) { + await this.setRefreshToken(refreshToken, vaultTimeoutAction, vaultTimeout, userId); + } + if (clientIdClientSecret != null) { await this.setClientId(clientIdClientSecret[0], vaultTimeoutAction, vaultTimeout, userId); await this.setClientSecret(clientIdClientSecret[1], vaultTimeoutAction, vaultTimeout, userId); } } + private async getAccessTokenKey(userId: UserId): Promise { + const accessTokenKeyB64 = await this.secureStorageService.get< + ReturnType + >(`${userId}${this.accessTokenKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); + + if (!accessTokenKeyB64) { + return null; + } + + const accessTokenKey = SymmetricCryptoKey.fromJSON(accessTokenKeyB64) as AccessTokenKey; + return accessTokenKey; + } + + private async createAndSaveAccessTokenKey(userId: UserId): Promise { + const newAccessTokenKey = (await this.keyGenerationService.createKey(512)) as AccessTokenKey; + + await this.secureStorageService.save( + `${userId}${this.accessTokenKeySecureStorageKey}`, + newAccessTokenKey, + this.getSecureStorageOptions(userId), + ); + + return newAccessTokenKey; + } + + private async clearAccessTokenKey(userId: UserId): Promise { + await this.secureStorageService.remove( + `${userId}${this.accessTokenKeySecureStorageKey}`, + this.getSecureStorageOptions(userId), + ); + } + + private async getOrCreateAccessTokenKey(userId: UserId): Promise { + if (!this.platformSupportsSecureStorage) { + throw new Error("Platform does not support secure storage. Cannot obtain access token key."); + } + + if (!userId) { + throw new Error("User id not found. Cannot obtain access token key."); + } + + // First see if we have an accessTokenKey in secure storage and return it if we do + let accessTokenKey: AccessTokenKey = await this.getAccessTokenKey(userId); + + if (!accessTokenKey) { + // Otherwise, create a new one and save it to secure storage, then return it + accessTokenKey = await this.createAndSaveAccessTokenKey(userId); + } + + return accessTokenKey; + } + + private async encryptAccessToken(accessToken: string, userId: UserId): Promise { + const accessTokenKey = await this.getOrCreateAccessTokenKey(userId); + + return await this.encryptService.encrypt(accessToken, accessTokenKey); + } + + private async decryptAccessToken( + encryptedAccessToken: EncString, + userId: UserId, + ): Promise { + const accessTokenKey = await this.getAccessTokenKey(userId); + + if (!accessTokenKey) { + // If we don't have an accessTokenKey, then that means we don't have an access token as it hasn't been set yet + // and we have to return null here to properly indicate the the user isn't logged in. + return null; + } + + const decryptedAccessToken = await this.encryptService.decryptToUtf8( + encryptedAccessToken, + accessTokenKey, + ); + + return decryptedAccessToken; + } + /** * Internal helper for set access token which always requires user id. * This is useful because setTokens always will have a user id from the access token whereas @@ -173,26 +281,33 @@ export class TokenService implements TokenServiceAbstraction { ); switch (storageLocation) { - case TokenStorageLocation.SecureStorage: - await this.saveStringToSecureStorage(userId, this.accessTokenSecureStorageKey, accessToken); + case TokenStorageLocation.SecureStorage: { + // Secure storage implementations have variable length limitations (Windows), so we cannot + // store the access token directly. Instead, we encrypt with accessTokenKey and store that + // in secure storage. + + const encryptedAccessToken: EncString = await this.encryptAccessToken(accessToken, userId); + + // Save the encrypted access token to disk + await this.singleUserStateProvider + .get(userId, ACCESS_TOKEN_DISK) + .update((_) => encryptedAccessToken.encryptedString); // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 - // 2024-02-20: Remove access token from memory and disk so that we migrate to secure storage over time. - // Remove these 2 calls to remove the access token from memory and disk after 3 releases. - - await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).update((_) => null); + // 2024-02-20: Remove access token from memory so that we migrate to encrypt the access token over time. + // Remove this call to remove the access token from memory after 3 releases. await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); - // Set flag to indicate that the access token has been migrated to secure storage (don't remove this) - await this.setAccessTokenMigratedToSecureStorage(userId); - return; + } case TokenStorageLocation.Disk: + // Access token stored on disk unencrypted as platform does not support secure storage await this.singleUserStateProvider .get(userId, ACCESS_TOKEN_DISK) .update((_) => accessToken); return; case TokenStorageLocation.Memory: + // Access token stored in memory due to vault timeout settings await this.singleUserStateProvider .get(userId, ACCESS_TOKEN_MEMORY) .update((_) => accessToken); @@ -226,15 +341,14 @@ export class TokenService implements TokenServiceAbstraction { throw new Error("User id not found. Cannot clear access token."); } - // TODO: re-eval this once we get shared key definitions for vault timeout and vault timeout action data. + // TODO: re-eval this implementation once we get shared key definitions for vault timeout and vault timeout action data. // we can't determine storage location w/out vaultTimeoutAction and vaultTimeout - // but we can simply clear all locations to avoid the need to require those parameters + // but we can simply clear all locations to avoid the need to require those parameters. if (this.platformSupportsSecureStorage) { - await this.secureStorageService.remove( - `${userId}${this.accessTokenSecureStorageKey}`, - this.getSecureStorageOptions(userId), - ); + // Always clear the access token key when clearing the access token + // The next set of the access token will create a new access token key + await this.clearAccessTokenKey(userId); } // Platform doesn't support secure storage, so use state provider implementation @@ -249,36 +363,48 @@ export class TokenService implements TokenServiceAbstraction { return undefined; } - const accessTokenMigratedToSecureStorage = - await this.getAccessTokenMigratedToSecureStorage(userId); - if (this.platformSupportsSecureStorage && accessTokenMigratedToSecureStorage) { - return await this.getStringFromSecureStorage(userId, this.accessTokenSecureStorageKey); - } - // Try to get the access token from memory const accessTokenMemory = await this.getStateValueByUserIdAndKeyDef( userId, ACCESS_TOKEN_MEMORY, ); - if (accessTokenMemory != null) { return accessTokenMemory; } // If memory is null, read from disk - return await this.getStateValueByUserIdAndKeyDef(userId, ACCESS_TOKEN_DISK); - } + const accessTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, ACCESS_TOKEN_DISK); + if (!accessTokenDisk) { + return null; + } - private async getAccessTokenMigratedToSecureStorage(userId: UserId): Promise { - return await firstValueFrom( - this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE).state$, - ); - } + if (this.platformSupportsSecureStorage) { + const accessTokenKey = await this.getAccessTokenKey(userId); - private async setAccessTokenMigratedToSecureStorage(userId: UserId): Promise { - await this.singleUserStateProvider - .get(userId, ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .update((_) => true); + if (!accessTokenKey) { + // We know this is an unencrypted access token because we don't have an access token key + return accessTokenDisk; + } + + try { + const encryptedAccessTokenEncString = new EncString(accessTokenDisk as EncryptedString); + + const decryptedAccessToken = await this.decryptAccessToken( + encryptedAccessTokenEncString, + userId, + ); + return decryptedAccessToken; + } catch (error) { + // If an error occurs during decryption, return null for logout. + // We don't try to recover here since we'd like to know + // if access token and key are getting out of sync. + this.logService.error( + `Failed to decrypt access token: ${error?.message ?? "Unknown error."}`, + ); + return null; + } + } + return accessTokenDisk; } // Private because we only ever set the refresh token when also setting the access token @@ -314,9 +440,6 @@ export class TokenService implements TokenServiceAbstraction { await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null); - // Set flag to indicate that the refresh token has been migrated to secure storage (don't remove this) - await this.setRefreshTokenMigratedToSecureStorage(userId); - return; case TokenStorageLocation.Disk: @@ -340,12 +463,6 @@ export class TokenService implements TokenServiceAbstraction { return undefined; } - const refreshTokenMigratedToSecureStorage = - await this.getRefreshTokenMigratedToSecureStorage(userId); - if (this.platformSupportsSecureStorage && refreshTokenMigratedToSecureStorage) { - return await this.getStringFromSecureStorage(userId, this.refreshTokenSecureStorageKey); - } - // pre-secure storage migration: // Always read memory first b/c faster const refreshTokenMemory = await this.getStateValueByUserIdAndKeyDef( @@ -357,13 +474,24 @@ export class TokenService implements TokenServiceAbstraction { return refreshTokenMemory; } - // if memory is null, read from disk + // if memory is null, read from disk and then secure storage const refreshTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, REFRESH_TOKEN_DISK); if (refreshTokenDisk != null) { return refreshTokenDisk; } + if (this.platformSupportsSecureStorage) { + const refreshTokenSecureStorage = await this.getStringFromSecureStorage( + userId, + this.refreshTokenSecureStorageKey, + ); + + if (refreshTokenSecureStorage != null) { + return refreshTokenSecureStorage; + } + } + return null; } @@ -389,18 +517,6 @@ export class TokenService implements TokenServiceAbstraction { await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); } - private async getRefreshTokenMigratedToSecureStorage(userId: UserId): Promise { - return await firstValueFrom( - this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE).state$, - ); - } - - private async setRefreshTokenMigratedToSecureStorage(userId: UserId): Promise { - await this.singleUserStateProvider - .get(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE) - .update((_) => true); - } - async setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, @@ -417,7 +533,7 @@ export class TokenService implements TokenServiceAbstraction { const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, - false, + false, // don't use secure storage for client id ); if (storageLocation === TokenStorageLocation.Disk) { @@ -484,7 +600,7 @@ export class TokenService implements TokenServiceAbstraction { const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, - false, + false, // don't use secure storage for client secret ); if (storageLocation === TokenStorageLocation.Disk) { @@ -567,6 +683,7 @@ export class TokenService implements TokenServiceAbstraction { }); } + // TODO: stop accepting optional userIds async clearTokens(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); @@ -735,7 +852,7 @@ export class TokenService implements TokenServiceAbstraction { private async getStateValueByUserIdAndKeyDef( userId: UserId, - storageLocation: KeyDefinition, + storageLocation: UserKeyDefinition, ): Promise { // read from single user state provider return await firstValueFrom(this.singleUserStateProvider.get(userId, storageLocation).state$); diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index f4089a73fb..dc00fec383 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -1,9 +1,8 @@ -import { KeyDefinition } from "../../platform/state"; +import { KeyDefinition, UserKeyDefinition } from "../../platform/state"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, - ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, API_KEY_CLIENT_ID_DISK, API_KEY_CLIENT_ID_MEMORY, API_KEY_CLIENT_SECRET_DISK, @@ -11,16 +10,13 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, } from "./token.state"; describe.each([ [ACCESS_TOKEN_DISK, "accessTokenDisk"], [ACCESS_TOKEN_MEMORY, "accessTokenMemory"], - [ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE, true], [REFRESH_TOKEN_DISK, "refreshTokenDisk"], [REFRESH_TOKEN_MEMORY, "refreshTokenMemory"], - [REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, true], [EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, { user: "token" }], [API_KEY_CLIENT_ID_DISK, "apiKeyClientIdDisk"], [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], @@ -30,8 +26,8 @@ describe.each([ "deserializes state key definitions", ( keyDefinition: - | KeyDefinition - | KeyDefinition + | UserKeyDefinition + | UserKeyDefinition | KeyDefinition>, state: string | boolean | Record, ) => { @@ -52,7 +48,10 @@ describe.each([ return typeof value === "object" && value !== null && !Array.isArray(value); } - function testDeserialization(keyDefinition: KeyDefinition, state: T) { + function testDeserialization( + keyDefinition: KeyDefinition | UserKeyDefinition, + state: T, + ) { const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state))); expect(deserialized).toEqual(state); } diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 022f56f7aa..458d6846c1 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -1,36 +1,34 @@ -import { KeyDefinition, TOKEN_DISK, TOKEN_DISK_LOCAL, TOKEN_MEMORY } from "../../platform/state"; - -export const ACCESS_TOKEN_DISK = new KeyDefinition(TOKEN_DISK, "accessToken", { - deserializer: (accessToken) => accessToken, -}); - -export const ACCESS_TOKEN_MEMORY = new KeyDefinition(TOKEN_MEMORY, "accessToken", { - deserializer: (accessToken) => accessToken, -}); - -export const ACCESS_TOKEN_MIGRATED_TO_SECURE_STORAGE = new KeyDefinition( +import { + KeyDefinition, TOKEN_DISK, - "accessTokenMigratedToSecureStorage", - { - deserializer: (accessTokenMigratedToSecureStorage) => accessTokenMigratedToSecureStorage, - }, -); + TOKEN_DISK_LOCAL, + TOKEN_MEMORY, + UserKeyDefinition, +} from "../../platform/state"; -export const REFRESH_TOKEN_DISK = new KeyDefinition(TOKEN_DISK, "refreshToken", { - deserializer: (refreshToken) => refreshToken, +// Note: all tokens / API key information must be cleared on logout. +// because we are using secure storage, we must manually call to clean up our tokens. +// See stateService.deAuthenticateAccount for where we call clearTokens(...) + +export const ACCESS_TOKEN_DISK = new UserKeyDefinition(TOKEN_DISK, "accessToken", { + deserializer: (accessToken) => accessToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_MEMORY = new KeyDefinition(TOKEN_MEMORY, "refreshToken", { - deserializer: (refreshToken) => refreshToken, +export const ACCESS_TOKEN_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "accessToken", { + deserializer: (accessToken) => accessToken, + clearOn: [], // Manually handled }); -export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new KeyDefinition( - TOKEN_DISK, - "refreshTokenMigratedToSecureStorage", - { - deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage, - }, -); +export const REFRESH_TOKEN_DISK = new UserKeyDefinition(TOKEN_DISK, "refreshToken", { + deserializer: (refreshToken) => refreshToken, + clearOn: [], // Manually handled +}); + +export const REFRESH_TOKEN_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "refreshToken", { + deserializer: (refreshToken) => refreshToken, + clearOn: [], // Manually handled +}); export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record( TOKEN_DISK_LOCAL, @@ -40,26 +38,34 @@ export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record(TOKEN_DISK, "apiKeyClientId", { +export const API_KEY_CLIENT_ID_DISK = new UserKeyDefinition(TOKEN_DISK, "apiKeyClientId", { deserializer: (apiKeyClientId) => apiKeyClientId, + clearOn: [], // Manually handled }); -export const API_KEY_CLIENT_ID_MEMORY = new KeyDefinition(TOKEN_MEMORY, "apiKeyClientId", { - deserializer: (apiKeyClientId) => apiKeyClientId, -}); +export const API_KEY_CLIENT_ID_MEMORY = new UserKeyDefinition( + TOKEN_MEMORY, + "apiKeyClientId", + { + deserializer: (apiKeyClientId) => apiKeyClientId, + clearOn: [], // Manually handled + }, +); -export const API_KEY_CLIENT_SECRET_DISK = new KeyDefinition( +export const API_KEY_CLIENT_SECRET_DISK = new UserKeyDefinition( TOKEN_DISK, "apiKeyClientSecret", { deserializer: (apiKeyClientSecret) => apiKeyClientSecret, + clearOn: [], // Manually handled }, ); -export const API_KEY_CLIENT_SECRET_MEMORY = new KeyDefinition( +export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition( TOKEN_MEMORY, "apiKeyClientSecret", { deserializer: (apiKeyClientSecret) => apiKeyClientSecret, + clearOn: [], // Manually handled }, ); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 03e267d9db..5a443b784d 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -10,7 +10,10 @@ import { LogService } from "../../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum"; +import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; +import { AccountService } from "../../abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "../../enums/verification-type"; @@ -35,6 +38,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti constructor( private stateService: StateService, private cryptoService: CryptoService, + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private i18nService: I18nService, private userVerificationApiService: UserVerificationApiServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, @@ -107,7 +112,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti if (verification.type === VerificationType.OTP) { request.otp = verification.secret; } else { - let masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey && !alreadyHashed) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -140,7 +146,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti case VerificationType.MasterPassword: return this.verifyUserByMasterPassword(verification); case VerificationType.PIN: - break; + return this.verifyUserByPIN(verification); case VerificationType.Biometrics: return this.verifyUserByBiometrics(); default: { @@ -164,7 +170,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private async verifyUserByMasterPassword( verification: MasterPasswordVerification, ): Promise { - let masterKey = await this.cryptoService.getMasterKey(); + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey) { masterKey = await this.cryptoService.makeMasterKey( verification.secret, @@ -181,7 +188,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti throw new Error(this.i18nService.t("invalidMasterPassword")); } // TODO: we should re-evaluate later on if user verification should have the side effect of modifying state. Probably not. - await this.cryptoService.setMasterKey(masterKey); + await this.masterPasswordService.setMasterKey(masterKey, userId); return true; } @@ -230,9 +237,10 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise { + userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; return ( (await this.hasMasterPassword(userId)) && - (await this.cryptoService.getMasterKeyHash()) != null + (await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId as UserId))) != null ); } diff --git a/libs/common/src/autofill/services/autofill-settings.service.ts b/libs/common/src/autofill/services/autofill-settings.service.ts index 49d6dc40de..eb6191d10b 100644 --- a/libs/common/src/autofill/services/autofill-settings.service.ts +++ b/libs/common/src/autofill/services/autofill-settings.service.ts @@ -9,40 +9,46 @@ import { GlobalState, KeyDefinition, StateProvider, + UserKeyDefinition, } from "../../platform/state"; import { ClearClipboardDelay, AutofillOverlayVisibility } from "../constants"; import { ClearClipboardDelaySetting, InlineMenuVisibilitySetting } from "../types"; -const AUTOFILL_ON_PAGE_LOAD = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "autofillOnPageLoad", { +const AUTOFILL_ON_PAGE_LOAD = new UserKeyDefinition(AUTOFILL_SETTINGS_DISK, "autofillOnPageLoad", { deserializer: (value: boolean) => value ?? false, + clearOn: [], }); -const AUTOFILL_ON_PAGE_LOAD_DEFAULT = new KeyDefinition( +const AUTOFILL_ON_PAGE_LOAD_DEFAULT = new UserKeyDefinition( AUTOFILL_SETTINGS_DISK, "autofillOnPageLoadDefault", { deserializer: (value: boolean) => value ?? false, + clearOn: [], }, ); -const AUTOFILL_ON_PAGE_LOAD_CALLOUT_DISMISSED = new KeyDefinition( +const AUTOFILL_ON_PAGE_LOAD_CALLOUT_DISMISSED = new UserKeyDefinition( AUTOFILL_SETTINGS_DISK, "autofillOnPageLoadCalloutIsDismissed", { deserializer: (value: boolean) => value ?? false, + clearOn: [], }, ); -const AUTOFILL_ON_PAGE_LOAD_POLICY_TOAST_HAS_DISPLAYED = new KeyDefinition( +const AUTOFILL_ON_PAGE_LOAD_POLICY_TOAST_HAS_DISPLAYED = new UserKeyDefinition( AUTOFILL_SETTINGS_DISK, "autofillOnPageLoadPolicyToastHasDisplayed", { deserializer: (value: boolean) => value ?? false, + clearOn: [], }, ); -const AUTO_COPY_TOTP = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "autoCopyTotp", { +const AUTO_COPY_TOTP = new UserKeyDefinition(AUTOFILL_SETTINGS_DISK, "autoCopyTotp", { deserializer: (value: boolean) => value ?? true, + clearOn: [], }); const INLINE_MENU_VISIBILITY = new KeyDefinition( @@ -57,11 +63,12 @@ const ENABLE_CONTEXT_MENU = new KeyDefinition(AUTOFILL_SETTINGS_DISK, "enableCon deserializer: (value: boolean) => value ?? true, }); -const CLEAR_CLIPBOARD_DELAY = new KeyDefinition( +const CLEAR_CLIPBOARD_DELAY = new UserKeyDefinition( AUTOFILL_SETTINGS_DISK_LOCAL, "clearClipboardDelay", { deserializer: (value: ClearClipboardDelaySetting) => value ?? ClearClipboardDelay.Never, + clearOn: [], }, ); diff --git a/libs/common/src/autofill/services/badge-settings.service.ts b/libs/common/src/autofill/services/badge-settings.service.ts index dcd266f885..e2f62b38b3 100644 --- a/libs/common/src/autofill/services/badge-settings.service.ts +++ b/libs/common/src/autofill/services/badge-settings.service.ts @@ -3,12 +3,13 @@ import { map, Observable } from "rxjs"; import { BADGE_SETTINGS_DISK, ActiveUserState, - KeyDefinition, StateProvider, + UserKeyDefinition, } from "../../platform/state"; -const ENABLE_BADGE_COUNTER = new KeyDefinition(BADGE_SETTINGS_DISK, "enableBadgeCounter", { +const ENABLE_BADGE_COUNTER = new UserKeyDefinition(BADGE_SETTINGS_DISK, "enableBadgeCounter", { deserializer: (value: boolean) => value ?? true, + clearOn: [], }); export abstract class BadgeSettingsServiceAbstraction { diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index 6ef4d10c0a..4b36e8d2bf 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -29,11 +29,12 @@ const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivale clearOn: ["logout"], }); -const DEFAULT_URI_MATCH_STRATEGY = new KeyDefinition( +const DEFAULT_URI_MATCH_STRATEGY = new UserKeyDefinition( DOMAIN_SETTINGS_DISK, "defaultUriMatchStrategy", { deserializer: (value: UriMatchStrategySetting) => value ?? UriMatchStrategy.Domain, + clearOn: [], }, ); diff --git a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts index 3982fa917b..1311976c4b 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,5 +1,7 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; +import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request"; +import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { cancelOrganizationSubscription: ( @@ -8,4 +10,10 @@ export abstract class BillingApiServiceAbstraction { ) => Promise; cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; getBillingStatus: (id: string) => Promise; + getProviderClientSubscriptions: (providerId: string) => Promise; + putProviderClientSubscriptions: ( + providerId: string, + organizationId: string, + request: ProviderSubscriptionUpdateRequest, + ) => Promise; } diff --git a/libs/common/src/billing/enums/plan-type.enum.ts b/libs/common/src/billing/enums/plan-type.enum.ts index 38febc50e4..c897770345 100644 --- a/libs/common/src/billing/enums/plan-type.enum.ts +++ b/libs/common/src/billing/enums/plan-type.enum.ts @@ -11,9 +11,14 @@ export enum PlanType { TeamsAnnually2020 = 9, EnterpriseMonthly2020 = 10, EnterpriseAnnually2020 = 11, - TeamsMonthly = 12, - TeamsAnnually = 13, - EnterpriseMonthly = 14, - EnterpriseAnnually = 15, - TeamsStarter = 16, + TeamsMonthly2023 = 12, + TeamsAnnually2023 = 13, + EnterpriseMonthly2023 = 14, + EnterpriseAnnually2023 = 15, + TeamsStarter2023 = 16, + TeamsMonthly = 17, + TeamsAnnually = 18, + EnterpriseMonthly = 19, + EnterpriseAnnually = 20, + TeamsStarter = 21, } diff --git a/libs/common/src/billing/models/billing-keys.state.ts b/libs/common/src/billing/models/billing-keys.state.ts index 8367ff7fbe..1d1cce6d0b 100644 --- a/libs/common/src/billing/models/billing-keys.state.ts +++ b/libs/common/src/billing/models/billing-keys.state.ts @@ -1,7 +1,7 @@ -import { BILLING_DISK, KeyDefinition } from "../../platform/state"; +import { BILLING_DISK, UserKeyDefinition } from "../../platform/state"; import { PaymentMethodWarning } from "../models/domain/payment-method-warning"; -export const PAYMENT_METHOD_WARNINGS_KEY = KeyDefinition.record( +export const PAYMENT_METHOD_WARNINGS_KEY = UserKeyDefinition.record( BILLING_DISK, "paymentMethodWarnings", { @@ -9,5 +9,6 @@ export const PAYMENT_METHOD_WARNINGS_KEY = KeyDefinition.record new Plans(i)); + } + } +} + +export class Plans extends BaseResponse { + planName: string; + seatMinimum: number; + assignedSeats: number; + purchasedSeats: number; + cost: number; + cadence: string; + + constructor(response: any) { + super(response); + this.planName = this.getResponseProperty("PlanName"); + this.seatMinimum = this.getResponseProperty("SeatMinimum"); + this.assignedSeats = this.getResponseProperty("AssignedSeats"); + this.purchasedSeats = this.getResponseProperty("PurchasedSeats"); + this.cost = this.getResponseProperty("Cost"); + this.cadence = this.getResponseProperty("Cadence"); + } +} diff --git a/libs/common/src/billing/models/response/subscription.response.ts b/libs/common/src/billing/models/response/subscription.response.ts index 0a2cb2290e..a05a40624d 100644 --- a/libs/common/src/billing/models/response/subscription.response.ts +++ b/libs/common/src/billing/models/response/subscription.response.ts @@ -36,6 +36,10 @@ export class BillingSubscriptionResponse extends BaseResponse { status: string; cancelled: boolean; items: BillingSubscriptionItemResponse[] = []; + collectionMethod: string; + suspensionDate?: string; + unpaidPeriodEndDate?: string; + gracePeriod?: number; constructor(response: any) { super(response); @@ -51,6 +55,10 @@ export class BillingSubscriptionResponse extends BaseResponse { if (items != null) { this.items = items.map((i: any) => new BillingSubscriptionItemResponse(i)); } + this.collectionMethod = this.getResponseProperty("CollectionMethod"); + this.suspensionDate = this.getResponseProperty("SuspensionDate"); + this.unpaidPeriodEndDate = this.getResponseProperty("unpaidPeriodEndDate"); + this.gracePeriod = this.getResponseProperty("GracePeriod"); } } diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts index 4a2a94e9c6..7f0f218a23 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts @@ -2,10 +2,10 @@ import { firstValueFrom } from "rxjs"; import { FakeAccountService, - FakeActiveUserStateProvider, mockAccountServiceWith, FakeActiveUserState, - trackEmissions, + FakeStateProvider, + FakeSingleUserState, } from "../../../../spec"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service"; @@ -16,20 +16,26 @@ import { } from "./billing-account-profile-state.service"; describe("BillingAccountProfileStateService", () => { - let activeUserStateProvider: FakeActiveUserStateProvider; + let stateProvider: FakeStateProvider; let sut: DefaultBillingAccountProfileStateService; let billingAccountProfileState: FakeActiveUserState; + let userBillingAccountProfileState: FakeSingleUserState; let accountService: FakeAccountService; const userId = "fakeUserId" as UserId; beforeEach(() => { accountService = mockAccountServiceWith(userId); - activeUserStateProvider = new FakeActiveUserStateProvider(accountService); + stateProvider = new FakeStateProvider(accountService); - sut = new DefaultBillingAccountProfileStateService(activeUserStateProvider); + sut = new DefaultBillingAccountProfileStateService(stateProvider); - billingAccountProfileState = activeUserStateProvider.getFake( + billingAccountProfileState = stateProvider.activeUser.getFake( + BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, + ); + + userBillingAccountProfileState = stateProvider.singleUser.getFake( + userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, ); }); @@ -38,9 +44,9 @@ describe("BillingAccountProfileStateService", () => { return jest.resetAllMocks(); }); - describe("accountHasPremiumFromAnyOrganization$", () => { - it("should emit changes in hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ + describe("hasPremiumFromAnyOrganization$", () => { + it("returns true when they have premium from an organization", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: false, hasPremiumFromAnyOrganization: true, }); @@ -48,118 +54,91 @@ describe("BillingAccountProfileStateService", () => { expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); }); - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumFromAnyOrganization$); - const startingEmissionCount = emissions.length; - - await sut.setHasPremium(true, true); - - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); - }); - }); - - describe("hasPremiumPersonally$", () => { - it("should emit changes in hasPremiumPersonally", async () => { - billingAccountProfileState.nextState({ - hasPremiumPersonally: true, - hasPremiumFromAnyOrganization: false, - }); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); - }); - - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumPersonally$); - const startingEmissionCount = emissions.length; - - await sut.setHasPremium(true, true); - - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); - }); - }); - - describe("canAccessPremium$", () => { - it("should emit changes in hasPremiumPersonally", async () => { - billingAccountProfileState.nextState({ - hasPremiumPersonally: true, - hasPremiumFromAnyOrganization: false, - }); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should emit changes in hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ + it("return false when they do not have premium from an organization", async () => { + userBillingAccountProfileState.nextState({ hasPremiumPersonally: false, - hasPremiumFromAnyOrganization: true, + hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should emit changes in both hasPremiumPersonally and hasPremiumFromAnyOrganization", async () => { - billingAccountProfileState.nextState({ - hasPremiumPersonally: true, - hasPremiumFromAnyOrganization: true, - }); - - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); - }); - - it("should emit once when calling setHasPremium once", async () => { - const emissions = trackEmissions(sut.hasPremiumFromAnySource$); - const startingEmissionCount = emissions.length; - - await sut.setHasPremium(true, true); - - const endingEmissionCount = emissions.length; - expect(endingEmissionCount - startingEmissionCount).toBe(1); - }); - }); - - describe("setHasPremium", () => { - it("should have `hasPremiumPersonally$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(true, false); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); - }); - - it("should have `hasPremiumFromAnyOrganization$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, true); - - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); - }); - - it("should have `hasPremiumPersonally$` emit `false` when passing `false` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(false, false); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); - }); - - it("should have `hasPremiumFromAnyOrganization$` emit `false` when passing `false` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, false); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); }); - it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumPersonally", async () => { - await sut.setHasPremium(true, false); + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); + + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); + }); + }); + + describe("hasPremiumPersonally$", () => { + it("returns true when the user has premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); + }); + + it("returns false when the user does not have premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); + }); + + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); + + expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); + }); + }); + + describe("hasPremiumFromAnySource$", () => { + it("returns true when the user has premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: false, + }); expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); - it("should have `canAccessPremium$` emit `true` when passing `true` as an argument for hasPremiumFromAnyOrganization", async () => { - await sut.setHasPremium(false, true); + it("returns true when the user has premium from an organization", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); }); - it("should have `canAccessPremium$` emit `false` when passing `false` for all arguments", async () => { - await sut.setHasPremium(false, false); + it("returns true when they have premium personally AND from an organization", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: true, + }); + + expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + }); + + it("returns false when there is no active user", async () => { + await accountService.switchAccount(null); expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false); }); }); + + describe("setHasPremium", () => { + it("should update the active users state when called", async () => { + await sut.setHasPremium(true, false); + + expect(billingAccountProfileState.nextMock).toHaveBeenCalledWith([ + userId, + { hasPremiumPersonally: true, hasPremiumFromAnyOrganization: false }, + ]); + }); + }); }); diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts index c6b6f104a8..cf05df2f22 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts @@ -1,21 +1,22 @@ -import { map, Observable } from "rxjs"; +import { map, Observable, of, switchMap } from "rxjs"; import { ActiveUserState, - ActiveUserStateProvider, BILLING_DISK, - KeyDefinition, + StateProvider, + UserKeyDefinition, } from "../../../platform/state"; import { BillingAccountProfile, BillingAccountProfileStateService, } from "../../abstractions/account/billing-account-profile-state.service"; -export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new KeyDefinition( +export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new UserKeyDefinition( BILLING_DISK, "accountProfile", { deserializer: (billingAccountProfile) => billingAccountProfile, + clearOn: ["logout"], }, ); @@ -26,24 +27,34 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP hasPremiumPersonally$: Observable; hasPremiumFromAnySource$: Observable; - constructor(activeUserStateProvider: ActiveUserStateProvider) { - this.billingAccountProfileState = activeUserStateProvider.get( + constructor(stateProvider: StateProvider) { + this.billingAccountProfileState = stateProvider.getActive( BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, ); - this.hasPremiumFromAnyOrganization$ = this.billingAccountProfileState.state$.pipe( + // Setup an observable that will always track the currently active user + // but will fallback to emitting null when there is no active user. + const billingAccountProfileOrNull = stateProvider.activeUserId$.pipe( + switchMap((userId) => + userId != null + ? stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).state$ + : of(null), + ), + ); + + this.hasPremiumFromAnyOrganization$ = billingAccountProfileOrNull.pipe( map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization), ); - this.hasPremiumPersonally$ = this.billingAccountProfileState.state$.pipe( + this.hasPremiumPersonally$ = billingAccountProfileOrNull.pipe( map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally), ); - this.hasPremiumFromAnySource$ = this.billingAccountProfileState.state$.pipe( + this.hasPremiumFromAnySource$ = billingAccountProfileOrNull.pipe( map( (billingAccountProfile) => - billingAccountProfile?.hasPremiumFromAnyOrganization || - billingAccountProfile?.hasPremiumPersonally, + billingAccountProfile?.hasPremiumFromAnyOrganization === true || + billingAccountProfile?.hasPremiumPersonally === true, ), ); } diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 3d0ff550ea..48866ab90d 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -2,6 +2,8 @@ import { ApiService } from "../../abstractions/api.service"; import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; +import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request"; +import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { constructor(private apiService: ApiService) {} @@ -34,4 +36,29 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new OrganizationBillingStatusResponse(r); } + + async getProviderClientSubscriptions(providerId: string): Promise { + const r = await this.apiService.send( + "GET", + "/providers/" + providerId + "/billing/subscription", + null, + true, + true, + ); + return new ProviderSubscriptionResponse(r); + } + + async putProviderClientSubscriptions( + providerId: string, + organizationId: string, + request: ProviderSubscriptionUpdateRequest, + ): Promise { + return await this.apiService.send( + "PUT", + "/providers/" + providerId + "/organizations/" + organizationId, + request, + true, + false, + ); + } } diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index a9437e288c..f2df30e4e0 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -81,6 +81,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs case PlanType.Free: case PlanType.FamiliesAnnually: case PlanType.FamiliesAnnually2019: + case PlanType.TeamsStarter2023: case PlanType.TeamsStarter: return true; default: diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 8a5075e96f..636e9bc4ce 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -2,12 +2,15 @@ export enum FeatureFlag { BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional - BulkCollectionAccess = "bulk-collection-access", VaultOnboarding = "vault-onboarding", GeneratorToolsModernization = "generator-tools-modernization", KeyRotationImprovements = "key-rotation-improvements", FlexibleCollectionsMigration = "flexible-collections-migration", ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners", + EnableConsolidatedBilling = "enable-consolidated-billing", + AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", + UnassignedItemsBanner = "unassigned-items-banner", + EnableDeleteProvider = "AC-1218-delete-provider", } // Replace this with a type safe lookup of the feature flag values in PM-2282 diff --git a/libs/common/src/platform/abstractions/app-id.service.ts b/libs/common/src/platform/abstractions/app-id.service.ts index c1414dd01f..c2c1a23ef5 100644 --- a/libs/common/src/platform/abstractions/app-id.service.ts +++ b/libs/common/src/platform/abstractions/app-id.service.ts @@ -1,8 +1,8 @@ import { Observable } from "rxjs"; export abstract class AppIdService { - appId$: Observable; - anonymousAppId$: Observable; - getAppId: () => Promise; - getAnonymousAppId: () => Promise; + abstract appId$: Observable; + abstract anonymousAppId$: Observable; + abstract getAppId(): Promise; + abstract getAnonymousAppId(): Promise; } diff --git a/libs/common/src/platform/abstractions/broadcaster.service.ts b/libs/common/src/platform/abstractions/broadcaster.service.ts index 5df3c03343..8abfb5a90c 100644 --- a/libs/common/src/platform/abstractions/broadcaster.service.ts +++ b/libs/common/src/platform/abstractions/broadcaster.service.ts @@ -9,13 +9,13 @@ export abstract class BroadcasterService { /** * @deprecated Use the observable from the appropriate service instead. */ - send: (message: MessageBase, id?: string) => void; + abstract send(message: MessageBase, id?: string): void; /** * @deprecated Use the observable from the appropriate service instead. */ - subscribe: (id: string, messageCallback: (message: MessageBase) => void) => void; + abstract subscribe(id: string, messageCallback: (message: MessageBase) => void): void; /** * @deprecated Use the observable from the appropriate service instead. */ - unsubscribe: (id: string) => void; + abstract unsubscribe(id: string): void; } diff --git a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts index 2b25164e7c..3c191f59cc 100644 --- a/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts +++ b/libs/common/src/platform/abstractions/config/config-api.service.abstraction.ts @@ -1,5 +1,9 @@ +import { UserId } from "../../../types/guid"; import { ServerConfigResponse } from "../../models/response/server-config.response"; export abstract class ConfigApiServiceAbstraction { - get: () => Promise; + /** + * Fetches the server configuration for the given user. If no user is provided, the configuration will not contain user-specific context. + */ + abstract get(userId: UserId | undefined): Promise; } diff --git a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts b/libs/common/src/platform/abstractions/config/config.service.abstraction.ts deleted file mode 100644 index 1e1de9155f..0000000000 --- a/libs/common/src/platform/abstractions/config/config.service.abstraction.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Observable } from "rxjs"; -import { SemVer } from "semver"; - -import { FeatureFlag } from "../../../enums/feature-flag.enum"; -import { Region } from "../environment.service"; - -import { ServerConfig } from "./server-config"; - -export abstract class ConfigServiceAbstraction { - serverConfig$: Observable; - cloudRegion$: Observable; - getFeatureFlag$: ( - key: FeatureFlag, - defaultValue?: T, - ) => Observable; - getFeatureFlag: ( - key: FeatureFlag, - defaultValue?: T, - ) => Promise; - checkServerMeetsVersionRequirement$: ( - minimumRequiredServerVersion: SemVer, - ) => Observable; - - /** - * Force ConfigService to fetch an updated config from the server and emit it from serverConfig$ - * @deprecated The service implementation should subscribe to an observable and use that to trigger a new fetch from - * server instead - */ - triggerServerConfigFetch: () => void; -} diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts new file mode 100644 index 0000000000..9eca5891ac --- /dev/null +++ b/libs/common/src/platform/abstractions/config/config.service.ts @@ -0,0 +1,47 @@ +import { Observable } from "rxjs"; +import { SemVer } from "semver"; + +import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { Region } from "../environment.service"; + +import { ServerConfig } from "./server-config"; + +export abstract class ConfigService { + /** The server config of the currently active user */ + serverConfig$: Observable; + /** The cloud region of the currently active user */ + cloudRegion$: Observable; + /** + * Retrieves the value of a feature flag for the currently active user + * @param key The feature flag to retrieve + * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable + * @returns An observable that emits the value of the feature flag, updates as the server config changes + */ + getFeatureFlag$: ( + key: FeatureFlag, + defaultValue?: T, + ) => Observable; + /** + * Retrieves the value of a feature flag for the currently active user + * @param key The feature flag to retrieve + * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable + * @returns The value of the feature flag + */ + getFeatureFlag: ( + key: FeatureFlag, + defaultValue?: T, + ) => Promise; + /** + * Verifies whether the server version meets the minimum required version + * @param minimumRequiredServerVersion The minimum version required + * @returns True if the server version is greater than or equal to the minimum required version + */ + checkServerMeetsVersionRequirement$: ( + minimumRequiredServerVersion: SemVer, + ) => Observable; + + /** + * Triggers a check that the config for the currently active user is up-to-date. If it is not, it will be fetched from the server and stored. + */ + abstract ensureConfigFetched(): Promise; +} diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index 2fa250202e..287e359f18 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -7,7 +7,6 @@ import { } from "../../models/data/server-config.data"; const dayInMilliseconds = 24 * 3600 * 1000; -const eighteenHoursInMilliseconds = 18 * 3600 * 1000; export class ServerConfig { version: string; @@ -38,10 +37,6 @@ export class ServerConfig { return this.getAgeInMilliseconds() <= dayInMilliseconds; } - expiresSoon(): boolean { - return this.getAgeInMilliseconds() >= eighteenHoursInMilliseconds; - } - static fromJSON(obj: Jsonify): ServerConfig { if (obj == null) { return null; diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index db432abc34..18c14677dd 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -3,85 +3,85 @@ import { DecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoFunctionService { - pbkdf2: ( + abstract pbkdf2( password: string | Uint8Array, salt: string | Uint8Array, algorithm: "sha256" | "sha512", iterations: number, - ) => Promise; - argon2: ( + ): Promise; + abstract argon2( password: string | Uint8Array, salt: string | Uint8Array, iterations: number, memory: number, parallelism: number, - ) => Promise; - hkdf: ( + ): Promise; + abstract hkdf( ikm: Uint8Array, salt: string | Uint8Array, info: string | Uint8Array, outputByteSize: number, algorithm: "sha256" | "sha512", - ) => Promise; - hkdfExpand: ( + ): Promise; + abstract hkdfExpand( prk: Uint8Array, info: string | Uint8Array, outputByteSize: number, algorithm: "sha256" | "sha512", - ) => Promise; - hash: ( + ): Promise; + abstract hash( value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512" | "md5", - ) => Promise; - hmac: ( + ): Promise; + abstract hmac( value: Uint8Array, key: Uint8Array, algorithm: "sha1" | "sha256" | "sha512", - ) => Promise; - compare: (a: Uint8Array, b: Uint8Array) => Promise; - hmacFast: ( + ): Promise; + abstract compare(a: Uint8Array, b: Uint8Array): Promise; + abstract hmacFast( value: Uint8Array | string, key: Uint8Array | string, algorithm: "sha1" | "sha256" | "sha512", - ) => Promise; - compareFast: (a: Uint8Array | string, b: Uint8Array | string) => Promise; - aesEncrypt: (data: Uint8Array, iv: Uint8Array, key: Uint8Array) => Promise; - aesDecryptFastParameters: ( + ): Promise; + abstract compareFast(a: Uint8Array | string, b: Uint8Array | string): Promise; + abstract aesEncrypt(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise; + abstract aesDecryptFastParameters( data: string, iv: string, mac: string, key: SymmetricCryptoKey, - ) => DecryptParameters; - aesDecryptFast: ( + ): DecryptParameters; + abstract aesDecryptFast( parameters: DecryptParameters, mode: "cbc" | "ecb", - ) => Promise; - aesDecrypt: ( + ): Promise; + abstract aesDecrypt( data: Uint8Array, iv: Uint8Array, key: Uint8Array, mode: "cbc" | "ecb", - ) => Promise; - rsaEncrypt: ( + ): Promise; + abstract rsaEncrypt( data: Uint8Array, publicKey: Uint8Array, algorithm: "sha1" | "sha256", - ) => Promise; - rsaDecrypt: ( + ): Promise; + abstract rsaDecrypt( data: Uint8Array, privateKey: Uint8Array, algorithm: "sha1" | "sha256", - ) => Promise; - rsaExtractPublicKey: (privateKey: Uint8Array) => Promise; - rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[Uint8Array, Uint8Array]>; + ): Promise; + abstract rsaExtractPublicKey(privateKey: Uint8Array): Promise; + abstract rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[Uint8Array, Uint8Array]>; /** * Generates a key of the given length suitable for use in AES encryption */ - aesGenerateKey: (bitLength: 128 | 192 | 256 | 512) => Promise; + abstract aesGenerateKey(bitLength: 128 | 192 | 256 | 512): Promise; /** * Generates a random array of bytes of the given length. Uses a cryptographically secure random number generator. * * Do not use this for generating encryption keys. Use aesGenerateKey or rsaGenerateKeyPair instead. */ - randomBytes: (length: number) => Promise; + abstract randomBytes(length: number): Promise; } diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index a5a2d45233..6609a1014e 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -12,115 +12,104 @@ import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoService { - activeUserKey$: Observable; + abstract activeUserKey$: Observable; + + /** + * Returns the an observable key for the given user id. + * + * @note this observable represents only user keys stored in memory. A null value does not indicate that we cannot load a user key from storage. + * @param userId The desired user + */ + abstract getInMemoryUserKeyFor$(userId: UserId): Observable; /** * Sets the provided user key and stores * any other necessary versions (such as auto, biometrics, * or pin) * - * @throws when key is null. Use {@link clearUserKey} instead + * @throws when key is null. Lock the account to clear a key * @param key The user key to set * @param userId The desired user */ - setUserKey: (key: UserKey, userId?: string) => Promise; + abstract setUserKey(key: UserKey, userId?: string): Promise; /** * Gets the user key from memory and sets it again, * kicking off a refresh of any additional keys * (such as auto, biometrics, or pin) */ - refreshAdditionalKeys: () => Promise; + abstract refreshAdditionalKeys(): Promise; /** * Observable value that returns whether or not the currently active user has ever had auser key, * i.e. has ever been unlocked/decrypted. This is key for differentiating between TDE locked and standard locked states. */ - everHadUserKey$: Observable; + abstract everHadUserKey$: Observable; /** * Retrieves the user key * @param userId The desired user * @returns The user key */ - getUserKey: (userId?: string) => Promise; + abstract getUserKey(userId?: string): Promise; /** * Checks if the user is using an old encryption scheme that used the master key * for encryption of data instead of the user key. */ - isLegacyUser: (masterKey?: MasterKey, userId?: string) => Promise; + abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise; /** * Use for encryption/decryption of data in order to support legacy * encryption models. It will return the user key if available, * if not it will return the master key. * @param userId The desired user */ - getUserKeyWithLegacySupport: (userId?: string) => Promise; + abstract getUserKeyWithLegacySupport(userId?: string): Promise; /** * Retrieves the user key from storage * @param keySuffix The desired version of the user's key to retrieve * @param userId The desired user * @returns The user key */ - getUserKeyFromStorage: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * Determines whether the user key is available for the given user. * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false. * @returns True if the user key is available */ - hasUserKey: (userId?: UserId) => Promise; + abstract hasUserKey(userId?: UserId): Promise; /** * Determines whether the user key is available for the given user in memory. * @param userId The desired user. If not provided, the active user will be used. If no active user exists, the method will return false. * @returns True if the user key is available */ - hasUserKeyInMemory: (userId?: string) => Promise; + abstract hasUserKeyInMemory(userId?: string): Promise; /** * @param keySuffix The desired version of the user's key to check * @param userId The desired user * @returns True if the provided version of the user key is stored */ - hasUserKeyStored: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * Generates a new user key * @param masterKey The user's master key * @returns A new user key and the master key protected version of it */ - makeUserKey: (key: MasterKey) => Promise<[UserKey, EncString]>; - /** - * Clears the user key - * @param clearStoredKeys Clears all stored versions of the user keys as well, - * such as the biometrics key - * @param userId The desired user - */ - clearUserKey: (clearSecretStorage?: boolean, userId?: string) => Promise; + abstract makeUserKey(key: MasterKey): Promise<[UserKey, EncString]>; /** * Clears the user's stored version of the user key * @param keySuffix The desired version of the key to clear * @param userId The desired user */ - clearStoredUserKey: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * Stores the master key encrypted user key * @param userKeyMasterKey The master key encrypted user key to set * @param userId The desired user */ - setMasterKeyEncryptedUserKey: (UserKeyMasterKey: string, userId?: string) => Promise; - /** - * Sets the user's master key - * @param key The user's master key to set - * @param userId The desired user - */ - setMasterKey: (key: MasterKey, userId?: string) => Promise; - /** - * @param userId The desired user - * @returns The user's master key - */ - getMasterKey: (userId?: string) => Promise; - + abstract setMasterKeyEncryptedUserKey(UserKeyMasterKey: string, userId?: string): Promise; /** * @param password The user's master password that will be used to derive a master key if one isn't found * @param userId The desired user */ - getOrDeriveMasterKey: (password: string, userId?: string) => Promise; + abstract getOrDeriveMasterKey(password: string, userId?: string): Promise; /** * Generates a master key from the provided password * @param password The user's master password @@ -129,17 +118,12 @@ export abstract class CryptoService { * @param KdfConfig The user's key derivation function configuration * @returns A master key derived from the provided password */ - makeMasterKey: ( + abstract makeMasterKey( password: string, email: string, kdf: KdfType, KdfConfig: KdfConfig, - ) => Promise; - /** - * Clears the user's master key - * @param userId The desired user - */ - clearMasterKey: (userId?: string) => Promise; + ): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -147,10 +131,10 @@ export abstract class CryptoService { * @param userKey The user key * @returns The user key and the master key protected version of it */ - encryptUserKeyWithMasterKey: ( + abstract encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, - ) => Promise<[UserKey, EncString]>; + ): Promise<[UserKey, EncString]>; /** * Decrypts the user key with the provided master key * @param masterKey The user's master key @@ -158,11 +142,11 @@ export abstract class CryptoService { * @param userId The desired user * @returns The user key */ - decryptUserKeyWithMasterKey: ( + abstract decryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: EncString, userId?: string, - ) => Promise; + ): Promise; /** * Creates a master password hash from the user's master password. Can * be used for local authentication or for server authentication depending @@ -172,21 +156,11 @@ export abstract class CryptoService { * @param hashPurpose The iterations to use for the hash * @returns The user's master password hash */ - hashMasterKey: (password: string, key: MasterKey, hashPurpose?: HashPurpose) => Promise; - /** - * Sets the user's master password hash - * @param keyHash The user's master password hash to set - */ - setMasterKeyHash: (keyHash: string) => Promise; - /** - * @returns The user's master password hash - */ - getMasterKeyHash: () => Promise; - /** - * Clears the user's stored master password hash - * @param userId The desired user - */ - clearMasterKeyHash: (userId?: string) => Promise; + abstract hashMasterKey( + password: string, + key: MasterKey, + hashPurpose?: HashPurpose, + ): Promise; /** * Compares the provided master password to the stored password hash and server password hash. * Updates the stored hash if outdated. @@ -195,107 +169,92 @@ export abstract class CryptoService { * @returns True if the provided master password matches either the stored * key hash or the server key hash */ - compareAndUpdateKeyHash: (masterPassword: string, masterKey: MasterKey) => Promise; + abstract compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise; /** * Stores the encrypted organization keys and clears any decrypted * organization keys currently in memory * @param orgs The organizations to set keys for * @param providerOrgs The provider organizations to set keys for */ - setOrgKeys: ( + abstract setOrgKeys( orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[], - ) => Promise; - activeUserOrgKeys$: Observable>; + ): Promise; + abstract activeUserOrgKeys$: Observable>; /** * Returns the organization's symmetric key * @deprecated Use the observable activeUserOrgKeys$ and `map` to the desired orgKey instead * @param orgId The desired organization * @returns The organization's symmetric key */ - getOrgKey: (orgId: string) => Promise; + abstract getOrgKey(orgId: string): Promise; /** * @deprecated Use the observable activeUserOrgKeys$ instead * @returns A record of the organization Ids to their symmetric keys */ - getOrgKeys: () => Promise>; + abstract getOrgKeys(): Promise>; /** * Uses the org key to derive a new symmetric key for encrypting data * @param orgKey The organization's symmetric key */ - makeDataEncKey: (key: T) => Promise<[SymmetricCryptoKey, EncString]>; - /** - * Clears the user's stored organization keys - * @param memoryOnly Clear only the in-memory keys - * @param userId The desired user - */ - clearOrgKeys: (memoryOnly?: boolean, userId?: string) => Promise; + abstract makeDataEncKey( + key: T, + ): Promise<[SymmetricCryptoKey, EncString]>; /** * Stores the encrypted provider keys and clears any decrypted * provider keys currently in memory * @param providers The providers to set keys for */ - activeUserProviderKeys$: Observable>; - setProviderKeys: (orgs: ProfileProviderResponse[]) => Promise; + abstract activeUserProviderKeys$: Observable>; + abstract setProviderKeys(orgs: ProfileProviderResponse[]): Promise; /** * @param providerId The desired provider * @returns The provider's symmetric key */ - getProviderKey: (providerId: string) => Promise; + abstract getProviderKey(providerId: string): Promise; /** * @returns A record of the provider Ids to their symmetric keys */ - getProviderKeys: () => Promise>; - /** - * @param memoryOnly Clear only the in-memory keys - * @param userId The desired user - */ - clearProviderKeys: (memoryOnly?: boolean, userId?: string) => Promise; + abstract getProviderKeys(): Promise>; /** * Returns the public key from memory. If not available, extracts it * from the private key and stores it in memory * @returns The user's public key */ - getPublicKey: () => Promise; + abstract getPublicKey(): Promise; /** * Creates a new organization key and encrypts it with the user's public key. * This method can also return Provider keys for creating new Provider users. * @returns The new encrypted org key and the decrypted key itself */ - makeOrgKey: () => Promise<[EncString, T]>; + abstract makeOrgKey(): Promise<[EncString, T]>; /** * Sets the the user's encrypted private key in storage and * clears the decrypted private key from memory * Note: does not clear the private key if null is provided * @param encPrivateKey An encrypted private key */ - setPrivateKey: (encPrivateKey: string) => Promise; + abstract setPrivateKey(encPrivateKey: string): Promise; /** * Returns the private key from memory. If not available, decrypts it * from storage and stores it in memory * @returns The user's private key */ - getPrivateKey: () => Promise; + abstract getPrivateKey(): Promise; /** * Generates a fingerprint phrase for the user based on their public key * @param fingerprintMaterial Fingerprint material * @param publicKey The user's public key * @returns The user's fingerprint phrase */ - getFingerprint: (fingerprintMaterial: string, publicKey?: Uint8Array) => Promise; + abstract getFingerprint(fingerprintMaterial: string, publicKey?: Uint8Array): Promise; /** * Generates a new keypair * @param key A key to encrypt the private key with. If not provided, * defaults to the user key * @returns A new keypair: [publicKey in Base64, encrypted privateKey] */ - makeKeyPair: (key?: SymmetricCryptoKey) => Promise<[string, EncString]>; - /** - * Clears the user's key pair - * @param memoryOnly Clear only the in-memory keys - * @param userId The desired user - */ - clearKeyPair: (memoryOnly?: boolean, userId?: string) => Promise; + abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>; /** * @param pin The user's pin * @param salt The user's salt @@ -303,14 +262,19 @@ export abstract class CryptoService { * @param kdfConfig The user's kdf config * @returns A key derived from the user's pin */ - makePinKey: (pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig) => Promise; + abstract makePinKey( + pin: string, + salt: string, + kdf: KdfType, + kdfConfig: KdfConfig, + ): Promise; /** * Clears the user's pin keys from storage * Note: This will remove the stored pin and as a result, * disable pin protection for the user * @param userId The desired user */ - clearPinKeys: (userId?: string) => Promise; + abstract clearPinKeys(userId?: string): Promise; /** * Decrypts the user key with their pin * @param pin The user's PIN @@ -321,13 +285,13 @@ export abstract class CryptoService { * it will be retrieved from storage * @returns The decrypted user key */ - decryptUserKeyWithPin: ( + abstract decryptUserKeyWithPin( pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, - ) => Promise; + ): Promise; /** * Creates a new Pin key that encrypts the user key instead of the * master key. Clears the old Pin key from state. @@ -340,55 +304,55 @@ export abstract class CryptoService { * places depending on if Master Password on Restart was enabled) * @returns The user key */ - decryptAndMigrateOldPinKey: ( + abstract decryptAndMigrateOldPinKey( masterPasswordOnRestart: boolean, pin: string, email: string, kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, - ) => Promise; + ): Promise; /** * Replaces old master auto keys with new user auto keys */ - migrateAutoKeyIfNeeded: (userId?: string) => Promise; + abstract migrateAutoKeyIfNeeded(userId?: string): Promise; /** * @param keyMaterial The key material to derive the send key from * @returns A new send key */ - makeSendKey: (keyMaterial: Uint8Array) => Promise; + abstract makeSendKey(keyMaterial: Uint8Array): Promise; /** * Clears all of the user's keys from storage * @param userId The user's Id */ - clearKeys: (userId?: string) => Promise; + abstract clearKeys(userId?: string): Promise; /** * RSA encrypts a value. * @param data The data to encrypt * @param publicKey The public key to use for encryption, if not provided, the user's public key will be used * @returns The encrypted data */ - rsaEncrypt: (data: Uint8Array, publicKey?: Uint8Array) => Promise; + abstract rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise; /** * Decrypts a value using RSA. * @param encValue The encrypted value to decrypt * @param privateKeyValue The private key to use for decryption * @returns The decrypted value */ - rsaDecrypt: (encValue: string, privateKeyValue?: Uint8Array) => Promise; - randomNumber: (min: number, max: number) => Promise; + abstract rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise; + abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key * @returns A new cipher key */ - makeCipherKey: () => Promise; + abstract makeCipherKey(): Promise; /** * Initialize all necessary crypto keys needed for a new account. * Warning! This completely replaces any existing keys! * @returns The user's newly created public key, private key, and encrypted private key */ - initAccount: () => Promise<{ + abstract initAccount(): Promise<{ userKey: UserKey; publicKey: string; privateKey: EncString; @@ -400,18 +364,18 @@ export abstract class CryptoService { * @remarks * Should always be called before updating a users KDF config. */ - validateKdfConfig: (kdf: KdfType, kdfConfig: KdfConfig) => void; + abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void; /** * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead. */ - decryptMasterKeyWithPin: ( + abstract decryptMasterKeyWithPin( pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, - ) => Promise; + ): Promise; /** * Previously, the master key was used for any additional key like the biometrics or pin key. * We have switched to using the user key for these purposes. This method is for clearing the state @@ -419,30 +383,36 @@ export abstract class CryptoService { * @param keySuffix The desired type of key to clear * @param userId The desired user */ - clearDeprecatedKeys: (keySuffix: KeySuffixOptions, userId?: string) => Promise; + abstract clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: string): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.encrypt */ - encrypt: (plainValue: string | Uint8Array, key?: SymmetricCryptoKey) => Promise; + abstract encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.encryptToBytes */ - encryptToBytes: (plainValue: Uint8Array, key?: SymmetricCryptoKey) => Promise; + abstract encryptToBytes( + plainValue: Uint8Array, + key?: SymmetricCryptoKey, + ): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.decryptToBytes */ - decryptToBytes: (encString: EncString, key?: SymmetricCryptoKey) => Promise; + abstract decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.decryptToUtf8 */ - decryptToUtf8: (encString: EncString, key?: SymmetricCryptoKey) => Promise; + abstract decryptToUtf8(encString: EncString, key?: SymmetricCryptoKey): Promise; /** * @deprecated July 25 2022: Get the key you need from CryptoService (getKeyForUserEncryption or getOrgKey) * and then call encryptService.decryptToBytes */ - decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise; + abstract decryptFromBytes( + encBuffer: EncArrayBuffer, + key: SymmetricCryptoKey, + ): Promise; } diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index a5120e6898..9b4dde3676 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -7,23 +7,26 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; - abstract encryptToBytes: ( + abstract encryptToBytes( plainValue: Uint8Array, key?: SymmetricCryptoKey, - ) => Promise; - abstract decryptToUtf8: (encString: EncString, key: SymmetricCryptoKey) => Promise; - abstract decryptToBytes: (encThing: Encrypted, key: SymmetricCryptoKey) => Promise; - abstract rsaEncrypt: (data: Uint8Array, publicKey: Uint8Array) => Promise; - abstract rsaDecrypt: (data: EncString, privateKey: Uint8Array) => Promise; - abstract resolveLegacyKey: (key: SymmetricCryptoKey, encThing: Encrypted) => SymmetricCryptoKey; - abstract decryptItems: ( + ): Promise; + abstract decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise; + abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise; + abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; + abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; + abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; + abstract decryptItems( items: Decryptable[], key: SymmetricCryptoKey, - ) => Promise; + ): Promise; /** * Generates a base64-encoded hash of the given value * @param value The value to hash * @param algorithm The hashing algorithm to use */ - hash: (value: string | Uint8Array, algorithm: "sha1" | "sha256" | "sha512") => Promise; + abstract hash( + value: string | Uint8Array, + algorithm: "sha1" | "sha256" | "sha512", + ): Promise; } diff --git a/libs/common/src/platform/abstractions/file-download/file-download.service.ts b/libs/common/src/platform/abstractions/file-download/file-download.service.ts index 44d082d72b..8bb70483eb 100644 --- a/libs/common/src/platform/abstractions/file-download/file-download.service.ts +++ b/libs/common/src/platform/abstractions/file-download/file-download.service.ts @@ -1,5 +1,5 @@ import { FileDownloadRequest } from "./file-download.request"; export abstract class FileDownloadService { - download: (request: FileDownloadRequest) => void; + abstract download(request: FileDownloadRequest): void; } diff --git a/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts b/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts index e6a323817c..5f26a66620 100644 --- a/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts +++ b/libs/common/src/platform/abstractions/file-upload/file-upload.service.ts @@ -3,12 +3,12 @@ import { EncArrayBuffer } from "../../models/domain/enc-array-buffer"; import { EncString } from "../../models/domain/enc-string"; export abstract class FileUploadService { - upload: ( + abstract upload( uploadData: { url: string; fileUploadType: FileUploadType }, fileName: EncString, encryptedFileData: EncArrayBuffer, fileUploadMethods: FileUploadApiMethods, - ) => Promise; + ): Promise; } export type FileUploadApiMethods = { diff --git a/libs/common/src/platform/abstractions/i18n.service.ts b/libs/common/src/platform/abstractions/i18n.service.ts index 7b6eb9edc8..a1b44d956a 100644 --- a/libs/common/src/platform/abstractions/i18n.service.ts +++ b/libs/common/src/platform/abstractions/i18n.service.ts @@ -3,8 +3,8 @@ import { Observable } from "rxjs"; import { TranslationService } from "./translation.service"; export abstract class I18nService extends TranslationService { - userSetLocale$: Observable; - locale$: Observable; + abstract userSetLocale$: Observable; + abstract locale$: Observable; abstract setLocale(locale: string): Promise; abstract init(): Promise; } diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts index a015182f89..223eb75038 100644 --- a/libs/common/src/platform/abstractions/key-generation.service.ts +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -11,7 +11,7 @@ export abstract class KeyGenerationService { * 512 bits = 64 bytes * @returns Generated key. */ - createKey: (bitLength: 256 | 512) => Promise; + abstract createKey(bitLength: 256 | 512): Promise; /** * Generates key material from CSPRNG and derives a 64 byte key from it. * Uses HKDF, see {@link https://datatracker.ietf.org/doc/html/rfc5869 RFC 5869} @@ -22,11 +22,11 @@ export abstract class KeyGenerationService { * @param salt Optional. If not provided will be generated from CSPRNG. * @returns An object containing the salt, key material, and derived key. */ - createKeyWithPurpose: ( + abstract createKeyWithPurpose( bitLength: 128 | 192 | 256 | 512, purpose: string, salt?: string, - ) => Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }>; + ): Promise<{ salt: string; material: CsprngArray; derivedKey: SymmetricCryptoKey }>; /** * Derives a 64 byte key from key material. * @remark The key material should be generated from {@link createKey}, or {@link createKeyWithPurpose}. @@ -37,11 +37,11 @@ export abstract class KeyGenerationService { * Different purposes results in different keys, even with the same material. * @returns 64 byte derived key. */ - deriveKeyFromMaterial: ( + abstract deriveKeyFromMaterial( material: CsprngArray, salt: string, purpose: string, - ) => Promise; + ): Promise; /** * Derives a 32 byte key from a password using a key derivation function. * @param password Password to derive the key from. @@ -50,10 +50,10 @@ export abstract class KeyGenerationService { * @param kdfConfig Configuration for the key derivation function. * @returns 32 byte derived key. */ - deriveKeyFromPassword: ( + abstract deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, kdf: KdfType, kdfConfig: KdfConfig, - ) => Promise; + ): Promise; } diff --git a/libs/common/src/platform/abstractions/log.service.ts b/libs/common/src/platform/abstractions/log.service.ts index 17db597687..dffa3ca8d3 100644 --- a/libs/common/src/platform/abstractions/log.service.ts +++ b/libs/common/src/platform/abstractions/log.service.ts @@ -1,9 +1,9 @@ import { LogLevelType } from "../enums/log-level-type.enum"; export abstract class LogService { - debug: (message: string) => void; - info: (message: string) => void; - warning: (message: string) => void; - error: (message: string) => void; - write: (level: LogLevelType, message: string) => void; + abstract debug(message: string): void; + abstract info(message: string): void; + abstract warning(message: string): void; + abstract error(message: string): void; + abstract write(level: LogLevelType, message: string): void; } diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index 7c5f05f919..ab4332c283 100644 --- a/libs/common/src/platform/abstractions/messaging.service.ts +++ b/libs/common/src/platform/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ export abstract class MessagingService { - send: (subscriber: string, arg?: any) => void; + abstract send(subscriber: string, arg?: any): void; } diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index 0053b7d1d7..d518a17f7b 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -12,34 +12,34 @@ export type ClipboardOptions = { }; export abstract class PlatformUtilsService { - getDevice: () => DeviceType; - getDeviceString: () => string; - getClientType: () => ClientType; - isFirefox: () => boolean; - isChrome: () => boolean; - isEdge: () => boolean; - isOpera: () => boolean; - isVivaldi: () => boolean; - isSafari: () => boolean; - isMacAppStore: () => boolean; - isViewOpen: () => Promise; - launchUri: (uri: string, options?: any) => void; - getApplicationVersion: () => Promise; - getApplicationVersionNumber: () => Promise; - supportsWebAuthn: (win: Window) => boolean; - supportsDuo: () => boolean; - showToast: ( + abstract getDevice(): DeviceType; + abstract getDeviceString(): string; + abstract getClientType(): ClientType; + abstract isFirefox(): boolean; + abstract isChrome(): boolean; + abstract isEdge(): boolean; + abstract isOpera(): boolean; + abstract isVivaldi(): boolean; + abstract isSafari(): boolean; + abstract isMacAppStore(): boolean; + abstract isViewOpen(): Promise; + abstract launchUri(uri: string, options?: any): void; + abstract getApplicationVersion(): Promise; + abstract getApplicationVersionNumber(): Promise; + abstract supportsWebAuthn(win: Window): boolean; + abstract supportsDuo(): boolean; + abstract showToast( type: "error" | "success" | "warning" | "info", title: string, text: string | string[], options?: ToastOptions, - ) => void; - isDev: () => boolean; - isSelfHost: () => boolean; - copyToClipboard: (text: string, options?: ClipboardOptions) => void | boolean; - readFromClipboard: () => Promise; - supportsBiometric: () => Promise; - authenticateBiometric: () => Promise; - supportsSecureStorage: () => boolean; - getAutofillKeyboardShortcut: () => Promise; + ): void; + abstract isDev(): boolean; + abstract isSelfHost(): boolean; + abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean; + abstract readFromClipboard(): Promise; + abstract supportsBiometric(): Promise; + abstract authenticateBiometric(): Promise; + abstract supportsSecureStorage(): boolean; + abstract getAutofillKeyboardShortcut(): Promise; } diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index b795db73fc..2348c8844a 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,26 +1,15 @@ import { Observable } from "rxjs"; -import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; -import { SendData } from "../../tools/send/models/data/send.data"; -import { SendView } from "../../tools/send/models/view/send.view"; import { UserId } from "../../types/guid"; -import { DeviceKey, MasterKey } from "../../types/key"; -import { CipherData } from "../../vault/models/data/cipher.data"; -import { LocalData } from "../../vault/models/data/local.data"; -import { CipherView } from "../../vault/models/view/cipher.view"; -import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { KdfType } from "../enums"; -import { ServerConfigData } from "../models/data/server-config.data"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; /** * Options for customizing the initiation behavior. @@ -39,38 +28,12 @@ export type InitOptions = { export abstract class StateService { accounts$: Observable<{ [userId: string]: T }>; activeAccount$: Observable; - /** - * @deprecated use accountService.activeAccount$ instead - */ - activeAccountUnlocked$: Observable; addAccount: (account: T) => Promise; setActiveUser: (userId: string) => Promise; clean: (options?: StorageOptions) => Promise; init: (initOptions?: InitOptions) => Promise; - getAddEditCipherInfo: (options?: StorageOptions) => Promise; - setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise; - getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; - setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; - getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; - setConvertAccountToKeyConnector: (value: boolean, options?: StorageOptions) => Promise; - /** - * Gets the user's master key - */ - getMasterKey: (options?: StorageOptions) => Promise; - /** - * Sets the user's master key - */ - setMasterKey: (value: MasterKey, options?: StorageOptions) => Promise; - /** - * Gets the user key encrypted by the master key - */ - getMasterKeyEncryptedUserKey: (options?: StorageOptions) => Promise; - /** - * Sets the user key encrypted by the master key - */ - setMasterKeyEncryptedUserKey: (value: string, options?: StorageOptions) => Promise; /** * Gets the user's auto key */ @@ -115,10 +78,6 @@ export abstract class StateService { * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; - /** - * @deprecated For legacy purposes only, use getMasterKey instead - */ - getCryptoMasterKey: (options?: StorageOptions) => Promise; /** * @deprecated For migration purposes only, use getUserKeyAuto instead */ @@ -139,8 +98,6 @@ export abstract class StateService { * @deprecated For migration purposes only, use setUserKeyBiometric instead */ setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise; - getDecryptedCiphers: (options?: StorageOptions) => Promise; - setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise; getDecryptedPasswordGenerationHistory: ( options?: StorageOptions, ) => Promise; @@ -156,27 +113,8 @@ export abstract class StateService { * @deprecated For migration purposes only, use setDecryptedUserKeyPin instead */ setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use SendService - */ - getDecryptedSends: (options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use SendService - */ - setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise; - getDisableGa: (options?: StorageOptions) => Promise; - setDisableGa: (value: boolean, options?: StorageOptions) => Promise; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; - getDeviceKey: (options?: StorageOptions) => Promise; - setDeviceKey: (value: DeviceKey | null, options?: StorageOptions) => Promise; - getAdminAuthRequest: (options?: StorageOptions) => Promise; - setAdminAuthRequest: ( - adminAuthRequest: AdminAuthRequestStorable, - options?: StorageOptions, - ) => Promise; - getShouldTrustDevice: (options?: StorageOptions) => Promise; - setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise; getEmail: (options?: StorageOptions) => Promise; setEmail: (value: string, options?: StorageOptions) => Promise; getEmailVerified: (options?: StorageOptions) => Promise; @@ -188,16 +126,6 @@ export abstract class StateService { value: boolean, options?: StorageOptions, ) => Promise; - getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise; - setEnableDuckDuckGoBrowserIntegration: ( - value: boolean, - options?: StorageOptions, - ) => Promise; - getEncryptedCiphers: (options?: StorageOptions) => Promise<{ [id: string]: CipherData }>; - setEncryptedCiphers: ( - value: { [id: string]: CipherData }, - options?: StorageOptions, - ) => Promise; getEncryptedPasswordGenerationHistory: ( options?: StorageOptions, ) => Promise; @@ -213,39 +141,17 @@ export abstract class StateService { * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use SendService - */ - getEncryptedSends: (options?: StorageOptions) => Promise<{ [id: string]: SendData }>; - /** - * @deprecated Do not call this directly, use SendService - */ - setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise; getEverBeenUnlocked: (options?: StorageOptions) => Promise; setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; - getForceSetPasswordReason: (options?: StorageOptions) => Promise; - setForceSetPasswordReason: ( - value: ForceSetPasswordReason, - options?: StorageOptions, - ) => Promise; - getInstalledVersion: (options?: StorageOptions) => Promise; - setInstalledVersion: (value: string, options?: StorageOptions) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; getKdfConfig: (options?: StorageOptions) => Promise; setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise; getKdfType: (options?: StorageOptions) => Promise; setKdfType: (value: KdfType, options?: StorageOptions) => Promise; - getKeyHash: (options?: StorageOptions) => Promise; - setKeyHash: (value: string, options?: StorageOptions) => Promise; getLastActive: (options?: StorageOptions) => Promise; setLastActive: (value: number, options?: StorageOptions) => Promise; getLastSync: (options?: StorageOptions) => Promise; setLastSync: (value: string, options?: StorageOptions) => Promise; - getLocalData: (options?: StorageOptions) => Promise<{ [cipherId: string]: LocalData }>; - setLocalData: ( - value: { [cipherId: string]: LocalData }, - options?: StorageOptions, - ) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; getOrganizationInvitation: (options?: StorageOptions) => Promise; @@ -270,39 +176,12 @@ export abstract class StateService { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; - getRememberedEmail: (options?: StorageOptions) => Promise; - setRememberedEmail: (value: string, options?: StorageOptions) => Promise; getSecurityStamp: (options?: StorageOptions) => Promise; setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; - getUsesKeyConnector: (options?: StorageOptions) => Promise; - setUsesKeyConnector: (value: boolean, options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; getVaultTimeoutAction: (options?: StorageOptions) => Promise; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; - getApproveLoginRequests: (options?: StorageOptions) => Promise; - setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use ConfigService - */ - getServerConfig: (options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use ConfigService - */ - setServerConfig: (value: ServerConfigData, options?: StorageOptions) => Promise; - /** - * fetches string value of URL user tried to navigate to while unauthenticated. - * @param options Defines the storage options for the URL; Defaults to session Storage. - * @returns route called prior to successful login. - */ - getDeepLinkRedirectUrl: (options?: StorageOptions) => Promise; - /** - * Store URL in session storage by default, but can be configured. Developed to handle - * unauthN interrupted navigation. - * @param url URL of route - * @param options Defines the storage options for the URL; Defaults to session Storage. - */ - setDeepLinkRedirectUrl: (url: string, options?: StorageOptions) => Promise; nextUpActiveUser: () => Promise; } diff --git a/libs/common/src/platform/abstractions/system.service.ts b/libs/common/src/platform/abstractions/system.service.ts index 5a7e11f9a1..204e336fbf 100644 --- a/libs/common/src/platform/abstractions/system.service.ts +++ b/libs/common/src/platform/abstractions/system.service.ts @@ -1,8 +1,8 @@ import { AuthService } from "../../auth/abstractions/auth.service"; export abstract class SystemService { - startProcessReload: (authService: AuthService) => Promise; - cancelProcessReload: () => void; - clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise; - clearPendingClipboard: () => Promise; + abstract startProcessReload(authService: AuthService): Promise; + abstract cancelProcessReload(): void; + abstract clearClipboard(clipboardValue: string, timeoutMs?: number): Promise; + abstract clearPendingClipboard(): Promise; } diff --git a/libs/common/src/platform/abstractions/translation.service.ts b/libs/common/src/platform/abstractions/translation.service.ts index 797965038a..8a8faff1d8 100644 --- a/libs/common/src/platform/abstractions/translation.service.ts +++ b/libs/common/src/platform/abstractions/translation.service.ts @@ -1,8 +1,8 @@ export abstract class TranslationService { - supportedTranslationLocales: string[]; - translationLocale: string; - collator: Intl.Collator; - localeNames: Map; - t: (id: string, p1?: string | number, p2?: string | number, p3?: string | number) => string; - translate: (id: string, p1?: string, p2?: string, p3?: string) => string; + abstract supportedTranslationLocales: string[]; + abstract translationLocale: string; + abstract collator: Intl.Collator; + abstract localeNames: Map; + abstract t(id: string, p1?: string | number, p2?: string | number, p3?: string | number): string; + abstract translate(id: string, p1?: string, p2?: string, p3?: string): string; } diff --git a/libs/common/src/platform/abstractions/validation.service.ts b/libs/common/src/platform/abstractions/validation.service.ts index c0985847bf..b5aa71381a 100644 --- a/libs/common/src/platform/abstractions/validation.service.ts +++ b/libs/common/src/platform/abstractions/validation.service.ts @@ -1,3 +1,3 @@ export abstract class ValidationService { - showError: (data: any) => string[]; + abstract showError(data: any): string[]; } diff --git a/libs/common/src/platform/biometrics/biometric-state.service.ts b/libs/common/src/platform/biometrics/biometric-state.service.ts index 82c05542b4..20bba49717 100644 --- a/libs/common/src/platform/biometrics/biometric-state.service.ts +++ b/libs/common/src/platform/biometrics/biometric-state.service.ts @@ -18,42 +18,42 @@ export abstract class BiometricStateService { /** * `true` if the currently active user has elected to store a biometric key to unlock their vault. */ - biometricUnlockEnabled$: Observable; // used to be biometricUnlock + abstract biometricUnlockEnabled$: Observable; // used to be biometricUnlock /** * If the user has elected to require a password on first unlock of an application instance, this key will store the * encrypted client key half used to unlock the vault. * * Tracks the currently active user */ - encryptedClientKeyHalf$: Observable; + abstract encryptedClientKeyHalf$: Observable; /** * whether or not a password is required on first unlock after opening the application * * tracks the currently active user */ - requirePasswordOnStart$: Observable; + abstract requirePasswordOnStart$: Observable; /** * Indicates the user has been warned about the security implications of using biometrics and, depending on the OS, * * tracks the currently active user. */ - dismissedRequirePasswordOnStartCallout$: Observable; + abstract dismissedRequirePasswordOnStartCallout$: Observable; /** * Whether the user has cancelled the biometric prompt. * * tracks the currently active user */ - promptCancelled$: Observable; + abstract promptCancelled$: Observable; /** * Whether the user has elected to automatically prompt for biometrics. * * tracks the currently active user */ - promptAutomatically$: Observable; + abstract promptAutomatically$: Observable; /** * Whether or not IPC fingerprint has been validated by the user this session. */ - fingerprintValidated$: Observable; + abstract fingerprintValidated$: Observable; /** * Updates the require password on start state for the currently active user. diff --git a/libs/common/src/platform/biometrics/biometric.state.spec.ts b/libs/common/src/platform/biometrics/biometric.state.spec.ts index 420a0fb86e..7bcccd2ea9 100644 --- a/libs/common/src/platform/biometrics/biometric.state.spec.ts +++ b/libs/common/src/platform/biometrics/biometric.state.spec.ts @@ -1,5 +1,5 @@ import { EncryptedString } from "../models/domain/enc-string"; -import { KeyDefinition } from "../state"; +import { KeyDefinition, UserKeyDefinition } from "../state"; import { BIOMETRIC_UNLOCK_ENABLED, @@ -22,9 +22,15 @@ describe.each([ ])( "deserializes state %s", ( - ...args: [KeyDefinition, EncryptedString] | [KeyDefinition, boolean] + ...args: + | [UserKeyDefinition, EncryptedString] + | [UserKeyDefinition, boolean] + | [KeyDefinition, boolean] ) => { - function testDeserialization(keyDefinition: KeyDefinition, state: T) { + function testDeserialization( + keyDefinition: UserKeyDefinition | KeyDefinition, + state: T, + ) { const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state))); expect(deserialized).toEqual(state); } diff --git a/libs/common/src/platform/biometrics/biometric.state.ts b/libs/common/src/platform/biometrics/biometric.state.ts index aa16e14baa..bcefb7b215 100644 --- a/libs/common/src/platform/biometrics/biometric.state.ts +++ b/libs/common/src/platform/biometrics/biometric.state.ts @@ -1,15 +1,16 @@ import { UserId } from "../../types/guid"; import { EncryptedString } from "../models/domain/enc-string"; -import { KeyDefinition, BIOMETRIC_SETTINGS_DISK } from "../state"; +import { KeyDefinition, BIOMETRIC_SETTINGS_DISK, UserKeyDefinition } from "../state"; /** * Indicates whether the user elected to store a biometric key to unlock their vault. */ -export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition( +export const BIOMETRIC_UNLOCK_ENABLED = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "biometricUnlockEnabled", { deserializer: (obj) => obj, + clearOn: [], }, ); @@ -18,11 +19,12 @@ export const BIOMETRIC_UNLOCK_ENABLED = new KeyDefinition( * * A true setting controls whether {@link ENCRYPTED_CLIENT_KEY_HALF} is set. */ -export const REQUIRE_PASSWORD_ON_START = new KeyDefinition( +export const REQUIRE_PASSWORD_ON_START = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "requirePasswordOnStart", { deserializer: (value) => value, + clearOn: [], }, ); @@ -33,11 +35,12 @@ export const REQUIRE_PASSWORD_ON_START = new KeyDefinition( * For operating systems without application-level key storage, this key half is concatenated with a signature * provided by the OS and used to encrypt the biometric key prior to storage. */ -export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition( +export const ENCRYPTED_CLIENT_KEY_HALF = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "clientKeyHalf", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); @@ -45,11 +48,12 @@ export const ENCRYPTED_CLIENT_KEY_HALF = new KeyDefinition( * Indicates the user has been warned about the security implications of using biometrics and, depending on the OS, * recommended to require a password on first unlock of an application instance. */ -export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new KeyDefinition( +export const DISMISSED_REQUIRE_PASSWORD_ON_START_CALLOUT = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "dismissedBiometricRequirePasswordOnStartCallout", { deserializer: (obj) => obj, + clearOn: [], }, ); @@ -68,11 +72,12 @@ export const PROMPT_CANCELLED = KeyDefinition.record( /** * Stores whether the user has elected to automatically prompt for biometric unlock on application start. */ -export const PROMPT_AUTOMATICALLY = new KeyDefinition( +export const PROMPT_AUTOMATICALLY = new UserKeyDefinition( BIOMETRIC_SETTINGS_DISK, "promptAutomatically", { deserializer: (obj) => obj, + clearOn: [], }, ); diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index c1f8d7757b..cc463b1060 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -1,5 +1,5 @@ // required to avoid linting errors when there are no flags -/* eslint-disable @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type SharedFlags = { multithreadDecryption: boolean; showPasswordless?: boolean; @@ -7,7 +7,7 @@ export type SharedFlags = { }; // required to avoid linting errors when there are no flags -/* eslint-disable @typescript-eslint/ban-types */ +// eslint-disable-next-line @typescript-eslint/ban-types export type SharedDevFlags = { noopNotifications: boolean; }; diff --git a/libs/common/src/platform/models/domain/account-keys.spec.ts b/libs/common/src/platform/models/domain/account-keys.spec.ts index 7041acc5ba..6bdb08edd5 100644 --- a/libs/common/src/platform/models/domain/account-keys.spec.ts +++ b/libs/common/src/platform/models/domain/account-keys.spec.ts @@ -1,10 +1,7 @@ import { makeStaticByteArray } from "../../../../spec"; -import { CsprngArray } from "../../../types/csprng"; -import { DeviceKey } from "../../../types/key"; import { Utils } from "../../misc/utils"; import { AccountKeys, EncryptionPair } from "./account"; -import { SymmetricCryptoKey } from "./symmetric-crypto-key"; describe("AccountKeys", () => { describe("toJSON", () => { @@ -24,23 +21,6 @@ describe("AccountKeys", () => { const json = JSON.stringify(keys); expect(json).toContain('"publicKey":"hello"'); }); - - // As the accountKeys.toJSON doesn't really serialize the device key - // this method just checks the persistence of the deviceKey - it("should persist deviceKey", () => { - // Arrange - const accountKeys = new AccountKeys(); - const deviceKeyBytesLength = 64; - accountKeys.deviceKey = new SymmetricCryptoKey( - new Uint8Array(deviceKeyBytesLength).buffer as CsprngArray, - ) as DeviceKey; - - // Act - const serializedKeys = accountKeys.toJSON(); - - // Assert - expect(serializedKeys.deviceKey).toEqual(accountKeys.deviceKey); - }); }); describe("fromJSON", () => { @@ -51,12 +31,6 @@ describe("AccountKeys", () => { expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello")); }); - it("should deserialize cryptoMasterKey", () => { - const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); - AccountKeys.fromJSON({} as any); - expect(spy).toHaveBeenCalled(); - }); - it("should deserialize privateKey", () => { const spy = jest.spyOn(EncryptionPair, "fromJSON"); AccountKeys.fromJSON({ @@ -64,24 +38,5 @@ describe("AccountKeys", () => { } as any); expect(spy).toHaveBeenCalled(); }); - - it("should deserialize deviceKey", () => { - // Arrange - const expectedKeyB64 = - "ZJNnhx9BbJeb2EAq1hlMjqt6GFsg9G/GzoFf6SbPKsaiMhKGDcbHcwcyEg56Lh8lfilpZz4SRM6UA7oFCg+lSg=="; - - const symmetricCryptoKeyFromJsonSpy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); - - // Act - const accountKeys = AccountKeys.fromJSON({ - deviceKey: { - keyB64: expectedKeyB64, - }, - } as any); - - // Assert - expect(symmetricCryptoKeyFromJsonSpy).toHaveBeenCalled(); - expect(accountKeys.deviceKey.keyB64).toEqual(expectedKeyB64); - }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 2657467ae6..ae7780ada4 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -1,7 +1,5 @@ import { Jsonify } from "type-fest"; -import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { @@ -9,16 +7,9 @@ import { PasswordGeneratorOptions, } from "../../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../../tools/generator/username/username-generation-options"; -import { SendData } from "../../../tools/send/models/data/send.data"; -import { SendView } from "../../../tools/send/models/view/send.view"; import { DeepJsonify } from "../../../types/deep-jsonify"; -import { MasterKey } from "../../../types/key"; -import { CipherData } from "../../../vault/models/data/cipher.data"; -import { CipherView } from "../../../vault/models/view/cipher.view"; -import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info"; import { KdfType } from "../../enums"; import { Utils } from "../../misc/utils"; -import { ServerConfigData } from "../../models/data/server-config.data"; import { EncryptedString, EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; @@ -66,48 +57,24 @@ export class DataEncryptionPair { decrypted?: TDecrypted[]; } -// This is a temporary structure to handle migrated `DataEncryptionPair` to -// avoid needing a data migration at this stage. It should be replaced with -// proper data migrations when `DataEncryptionPair` is deprecated. -export class TemporaryDataEncryption { - encrypted?: { [id: string]: TEncrypted }; -} - export class AccountData { - ciphers?: DataEncryptionPair = new DataEncryptionPair< - CipherData, - CipherView - >(); - localData?: any; - sends?: DataEncryptionPair = new DataEncryptionPair(); passwordGenerationHistory?: EncryptionPair< GeneratedPasswordHistory[], GeneratedPasswordHistory[] > = new EncryptionPair(); - addEditCipherInfo?: AddEditCipherInfo; static fromJSON(obj: DeepJsonify): AccountData { if (obj == null) { return null; } - return Object.assign(new AccountData(), obj, { - addEditCipherInfo: { - cipher: CipherView.fromJSON(obj?.addEditCipherInfo?.cipher), - collectionIds: obj?.addEditCipherInfo?.collectionIds, - }, - }); + return Object.assign(new AccountData(), obj); } } export class AccountKeys { - masterKey?: MasterKey; - masterKeyEncryptedUserKey?: string; - deviceKey?: ReturnType; publicKey?: Uint8Array; - /** @deprecated July 2023, left for migration purposes*/ - cryptoMasterKey?: SymmetricCryptoKey; /** @deprecated July 2023, left for migration purposes*/ cryptoMasterKeyAuto?: string; /** @deprecated July 2023, left for migration purposes*/ @@ -132,9 +99,6 @@ export class AccountKeys { return null; } return Object.assign(new AccountKeys(), obj, { - masterKey: SymmetricCryptoKey.fromJSON(obj?.masterKey), - deviceKey: obj?.deviceKey, - cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey), cryptoSymmetricKey: EncryptionPair.fromJSON( obj?.cryptoSymmetricKey, SymmetricCryptoKey.fromJSON, @@ -159,16 +123,12 @@ export class AccountKeys { } export class AccountProfile { - convertAccountToKeyConnector?: boolean; name?: string; email?: string; emailVerified?: boolean; everBeenUnlocked?: boolean; - forceSetPasswordReason?: ForceSetPasswordReason; lastSync?: string; userId?: string; - usesKeyConnector?: boolean; - keyHash?: string; kdfIterations?: number; kdfMemory?: number; kdfParallelism?: number; @@ -185,8 +145,6 @@ export class AccountProfile { export class AccountSettings { defaultUriMatch?: UriMatchStrategySetting; - disableGa?: boolean; - enableBiometric?: boolean; minimizeOnCopyToClipboard?: boolean; passwordGenerationOptions?: PasswordGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions; @@ -196,10 +154,6 @@ export class AccountSettings { protectedPin?: string; vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; - serverConfig?: ServerConfigData; - approveLoginRequests?: boolean; - avatarColor?: string; - trustDeviceChoiceForDecryption?: boolean; /** @deprecated July 2023, left for migration purposes*/ pinProtected?: EncryptionPair = new EncryptionPair(); @@ -214,7 +168,6 @@ export class AccountSettings { obj?.pinProtected, EncString.fromJSON, ), - serverConfig: ServerConfigData.fromJSON(obj?.serverConfig), }); } } @@ -237,7 +190,6 @@ export class Account { profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); tokens?: AccountTokens = new AccountTokens(); - adminAuthRequest?: Jsonify = null; constructor(init: Partial) { Object.assign(this, { @@ -261,7 +213,6 @@ export class Account { ...new AccountTokens(), ...init?.tokens, }, - adminAuthRequest: init?.adminAuthRequest, }); } @@ -276,7 +227,6 @@ export class Account { profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), tokens: AccountTokens.fromJSON(json?.tokens), - adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest), }); } } diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 3fd6f38200..703a998d1c 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -1,18 +1,7 @@ -import { ThemeType } from "../../enums"; - export class GlobalState { - installedVersion?: string; organizationInvitation?: any; - rememberedEmail?: string; - theme?: ThemeType = ThemeType.System; - twoFactorToken?: string; - biometricFingerprintValidated?: boolean; vaultTimeout?: number; vaultTimeoutAction?: string; - loginRedirect?: any; - mainWindowSize?: number; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; - enableDuckDuckGoBrowserIntegration?: boolean; - deepLinkRedirectUrl?: string; } diff --git a/libs/common/src/platform/services/app-id.service.spec.ts b/libs/common/src/platform/services/app-id.service.spec.ts index ae44bc95e0..10fb153fda 100644 --- a/libs/common/src/platform/services/app-id.service.spec.ts +++ b/libs/common/src/platform/services/app-id.service.spec.ts @@ -14,7 +14,7 @@ describe("AppIdService", () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.restoreAllMocks(); }); describe("getAppId", () => { diff --git a/libs/common/src/platform/services/config/config-api.service.ts b/libs/common/src/platform/services/config/config-api.service.ts index 702c38f53c..f283410ace 100644 --- a/libs/common/src/platform/services/config/config-api.service.ts +++ b/libs/common/src/platform/services/config/config-api.service.ts @@ -1,18 +1,20 @@ import { ApiService } from "../../../abstractions/api.service"; -import { AuthService } from "../../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { TokenService } from "../../../auth/abstractions/token.service"; +import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ServerConfigResponse } from "../../models/response/server-config.response"; export class ConfigApiService implements ConfigApiServiceAbstraction { constructor( private apiService: ApiService, - private authService: AuthService, + private tokenService: TokenService, ) {} - async get(): Promise { + async get(userId: UserId | undefined): Promise { + // Authentication adds extra context to config responses, if the user has an access token, we want to use it + // We don't particularly care about ensuring the token is valid and not expired, just that it exists const authed: boolean = - (await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut; + userId == null ? false : (await this.tokenService.getAccessToken(userId)) != null; const r = await this.apiService.send("GET", "/config", null, authed, true); return new ServerConfigResponse(r); diff --git a/libs/common/src/platform/services/config/config.service.spec.ts b/libs/common/src/platform/services/config/config.service.spec.ts index 7f337f3322..d643311a26 100644 --- a/libs/common/src/platform/services/config/config.service.spec.ts +++ b/libs/common/src/platform/services/config/config.service.spec.ts @@ -1,200 +1,264 @@ -import { MockProxy, mock } from "jest-mock-extended"; -import { ReplaySubject, skip, take } from "rxjs"; +/** + * need to update test environment so structuredClone works appropriately + * @jest-environment ../../libs/shared/test.environment.ts + */ -import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; -import { AuthService } from "../../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; +import { mock } from "jest-mock-extended"; +import { Subject, firstValueFrom, of } from "rxjs"; + +import { + FakeGlobalState, + FakeSingleUserState, + FakeStateProvider, + awaitAsync, + mockAccountServiceWith, +} from "../../../../spec"; +import { subscribeTo } from "../../../../spec/observable-tracker"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ServerConfig } from "../../abstractions/config/server-config"; import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { LogService } from "../../abstractions/log.service"; -import { StateService } from "../../abstractions/state.service"; +import { Utils } from "../../misc/utils"; import { ServerConfigData } from "../../models/data/server-config.data"; import { EnvironmentServerConfigResponse, ServerConfigResponse, ThirdPartyServerConfigResponse, } from "../../models/response/server-config.response"; -import { StateProvider } from "../../state"; -import { ConfigService } from "./config.service"; +import { + ApiUrl, + DefaultConfigService, + RETRIEVAL_INTERVAL, + GLOBAL_SERVER_CONFIGURATIONS, + USER_SERVER_CONFIG, +} from "./default-config.service"; describe("ConfigService", () => { - let stateService: MockProxy; - let configApiService: MockProxy; - let authService: MockProxy; - let environmentService: MockProxy; - let logService: MockProxy; - let replaySubject: ReplaySubject; - let stateProvider: StateProvider; - - let serverResponseCount: number; // increments to track distinct responses received from server - - // Observables will start emitting as soon as this is created, so only create it - // after everything is mocked - const configServiceFactory = () => { - const configService = new ConfigService( - stateService, - configApiService, - authService, - environmentService, - logService, - stateProvider, - ); - configService.init(); - return configService; - }; + const configApiService = mock(); + const environmentService = mock(); + const logService = mock(); + let stateProvider: FakeStateProvider; + let globalState: FakeGlobalState>; + let userState: FakeSingleUserState; + const activeApiUrl = apiUrl(0); + const userId = "userId" as UserId; + const accountService = mockAccountServiceWith(userId); + const tooOld = new Date(Date.now() - 1.1 * RETRIEVAL_INTERVAL); beforeEach(() => { - stateService = mock(); - configApiService = mock(); - authService = mock(); - environmentService = mock(); - logService = mock(); - replaySubject = new ReplaySubject(1); - const accountService = mockAccountServiceWith("0" as UserId); stateProvider = new FakeStateProvider(accountService); - - environmentService.environment$ = replaySubject.asObservable(); - - serverResponseCount = 1; - configApiService.get.mockImplementation(() => - Promise.resolve(serverConfigResponseFactory("server" + serverResponseCount++)), - ); - - jest.useFakeTimers(); + globalState = stateProvider.global.getFake(GLOBAL_SERVER_CONFIGURATIONS); + userState = stateProvider.singleUser.getFake(userId, USER_SERVER_CONFIG); }); afterEach(() => { - jest.useRealTimers(); + jest.resetAllMocks(); }); - it("Uses storage as fallback", (done) => { - const storedConfigData = serverConfigDataFactory("storedConfig"); - stateService.getServerConfig.mockResolvedValueOnce(storedConfigData); + describe.each([null, userId])("active user: %s", (activeUserId) => { + let sut: DefaultConfigService; - configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch")); - - const configService = configServiceFactory(); - - configService.serverConfig$.pipe(take(1)).subscribe((config) => { - expect(config).toEqual(new ServerConfig(storedConfigData)); - expect(stateService.getServerConfig).toHaveBeenCalledTimes(1); - expect(stateService.setServerConfig).not.toHaveBeenCalled(); - done(); + beforeAll(async () => { + await accountService.switchAccount(activeUserId); }); - configService.triggerServerConfigFetch(); - }); - - it("Stream does not error out if fetch fails", (done) => { - const storedConfigData = serverConfigDataFactory("storedConfig"); - stateService.getServerConfig.mockResolvedValueOnce(storedConfigData); - - const configService = configServiceFactory(); - - configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server1"); - done(); - } catch (e) { - done(e); - } - }); - - configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch")); - configService.triggerServerConfigFetch(); - - configApiService.get.mockResolvedValueOnce(serverConfigResponseFactory("server1")); - configService.triggerServerConfigFetch(); - }); - - describe("Fetches config from server", () => { beforeEach(() => { - stateService.getServerConfig.mockResolvedValueOnce(null); + environmentService.environment$ = of(environmentFactory(activeApiUrl)); + sut = new DefaultConfigService( + configApiService, + environmentService, + logService, + stateProvider, + ); }); - it.each([1, 2, 3])( - "after %p hour/s", - (hours: number, done: jest.DoneCallback) => { - const configService = configServiceFactory(); + describe("serverConfig$", () => { + it.each([{}, null])("handles null stored state", async (globalTestState) => { + globalState.stateSubject.next(globalTestState); + userState.nextState(null); + await expect(firstValueFrom(sut.serverConfig$)).resolves.not.toThrow(); + }); - // skip previous hours (if any) - configService.serverConfig$.pipe(skip(hours - 1), take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server" + hours); - expect(configApiService.get).toHaveBeenCalledTimes(hours); - done(); - } catch (e) { - done(e); - } + describe.each(["stale", "missing"])("%s config", (configStateDescription) => { + const userStored = + configStateDescription === "missing" + ? null + : serverConfigFactory(activeApiUrl + userId, tooOld); + const globalStored = + configStateDescription === "missing" + ? {} + : { + [activeApiUrl]: serverConfigFactory(activeApiUrl, tooOld), + }; + + beforeEach(() => { + globalState.stateSubject.next(globalStored); + userState.nextState(userStored); }); - const oneHourInMs = 1000 * 3600; - jest.advanceTimersByTime(oneHourInMs * hours + 1); - }, - ); + // sanity check + test("authed and unauthorized state are different", () => { + expect(globalStored[activeApiUrl]).not.toEqual(userStored); + }); - it("when environment URLs change", (done) => { - const configService = configServiceFactory(); + describe("fail to fetch", () => { + beforeEach(() => { + configApiService.get.mockRejectedValue(new Error("Unable to fetch")); + }); - configService.serverConfig$.pipe(take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server1"); - done(); - } catch (e) { - done(e); - } + it("uses storage as fallback", async () => { + const actual = await firstValueFrom(sut.serverConfig$); + expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); + expect(configApiService.get).toHaveBeenCalledTimes(1); + }); + + it("does not error out when fetch fails", async () => { + await expect(firstValueFrom(sut.serverConfig$)).resolves.not.toThrow(); + expect(configApiService.get).toHaveBeenCalledTimes(1); + }); + + it("logs an error when unable to fetch", async () => { + await firstValueFrom(sut.serverConfig$); + + expect(logService.error).toHaveBeenCalledWith( + `Unable to fetch ServerConfig from ${activeApiUrl}: Unable to fetch`, + ); + }); + }); + + describe("fetch success", () => { + const response = serverConfigResponseFactory(); + const newConfig = new ServerConfig(new ServerConfigData(response)); + + it("should be a new config", async () => { + expect(newConfig).not.toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); + }); + + it("fetches config from server when it's older than an hour", async () => { + await firstValueFrom(sut.serverConfig$); + + expect(configApiService.get).toHaveBeenCalledTimes(1); + }); + + it("returns the updated config", async () => { + configApiService.get.mockResolvedValue(response); + + const actual = await firstValueFrom(sut.serverConfig$); + + // This is the time the response is converted to a config + expect(actual.utcDate).toAlmostEqual(newConfig.utcDate, 1000); + delete actual.utcDate; + delete newConfig.utcDate; + + expect(actual).toEqual(newConfig); + }); + }); }); - replaySubject.next(null); - }); + describe("fresh configuration", () => { + const userStored = serverConfigFactory(activeApiUrl + userId); + const globalStored = { + [activeApiUrl]: serverConfigFactory(activeApiUrl), + }; + beforeEach(() => { + globalState.stateSubject.next(globalStored); + userState.nextState(userStored); + }); + it("does not fetch from server", async () => { + await firstValueFrom(sut.serverConfig$); - it("when triggerServerConfigFetch() is called", (done) => { - const configService = configServiceFactory(); + expect(configApiService.get).not.toHaveBeenCalled(); + }); - configService.serverConfig$.pipe(take(1)).subscribe((config) => { - try { - expect(config.gitHash).toEqual("server1"); - done(); - } catch (e) { - done(e); - } + it("uses stored value", async () => { + const actual = await firstValueFrom(sut.serverConfig$); + expect(actual).toEqual(activeUserId ? userStored : globalStored[activeApiUrl]); + }); + + it("does not complete after emit", async () => { + const emissions = []; + const subscription = sut.serverConfig$.subscribe((v) => emissions.push(v)); + await awaitAsync(); + expect(emissions.length).toBe(1); + expect(subscription.closed).toBe(false); + }); }); - - configService.triggerServerConfigFetch(); }); }); - it("Saves server config to storage when the user is logged in", (done) => { - stateService.getServerConfig.mockResolvedValueOnce(null); - authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked); - const configService = configServiceFactory(); + describe("environment change", () => { + let sut: DefaultConfigService; + let environmentSubject: Subject; - configService.serverConfig$.pipe(take(1)).subscribe(() => { - try { - expect(stateService.setServerConfig).toHaveBeenCalledWith( - expect.objectContaining({ gitHash: "server1" }), - ); - done(); - } catch (e) { - done(e); - } + beforeAll(async () => { + // updating environment with an active account is undefined behavior + await accountService.switchAccount(null); }); - configService.triggerServerConfigFetch(); + beforeEach(() => { + environmentSubject = new Subject(); + environmentService.environment$ = environmentSubject; + sut = new DefaultConfigService( + configApiService, + environmentService, + logService, + stateProvider, + ); + }); + + describe("serverConfig$", () => { + it("emits a new config when the environment changes", async () => { + const globalStored = { + [apiUrl(0)]: serverConfigFactory(apiUrl(0)), + [apiUrl(1)]: serverConfigFactory(apiUrl(1)), + }; + globalState.stateSubject.next(globalStored); + + const spy = subscribeTo(sut.serverConfig$); + + environmentSubject.next(environmentFactory(apiUrl(0))); + environmentSubject.next(environmentFactory(apiUrl(1))); + + const expected = [globalStored[apiUrl(0)], globalStored[apiUrl(1)]]; + + const actual = await spy.pauseUntilReceived(2); + expect(actual.length).toBe(2); + + // validate dates this is done separately because the dates are created when ServerConfig is initialized + expect(actual[0].utcDate).toAlmostEqual(expected[0].utcDate, 1000); + expect(actual[1].utcDate).toAlmostEqual(expected[1].utcDate, 1000); + delete actual[0].utcDate; + delete actual[1].utcDate; + delete expected[0].utcDate; + delete expected[1].utcDate; + + expect(actual).toEqual(expected); + spy.unsubscribe(); + }); + }); }); }); -function serverConfigDataFactory(gitHash: string) { - return new ServerConfigData(serverConfigResponseFactory(gitHash)); +function apiUrl(count: number) { + return `https://api${count}.test.com`; } -function serverConfigResponseFactory(gitHash: string) { +function serverConfigFactory(hash: string, date: Date = new Date()) { + const config = new ServerConfig(serverConfigDataFactory(hash)); + config.utcDate = date; + return config; +} + +function serverConfigDataFactory(hash?: string) { + return new ServerConfigData(serverConfigResponseFactory(hash)); +} + +function serverConfigResponseFactory(hash?: string) { return new ServerConfigResponse({ version: "myConfigVersion", - gitHash: gitHash, + gitHash: hash ?? Utils.newGuid(), // Use optional git hash to store uniqueness value server: new ThirdPartyServerConfigResponse({ name: "myThirdPartyServer", url: "www.example.com", @@ -209,3 +273,9 @@ function serverConfigResponseFactory(gitHash: string) { }, }); } + +function environmentFactory(apiUrl: string) { + return { + getApiUrl: () => apiUrl, + } as Environment; +} diff --git a/libs/common/src/platform/services/config/config.service.ts b/libs/common/src/platform/services/config/config.service.ts deleted file mode 100644 index 86948fc1c0..0000000000 --- a/libs/common/src/platform/services/config/config.service.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - ReplaySubject, - Subject, - catchError, - concatMap, - defer, - delayWhen, - firstValueFrom, - map, - merge, - timer, -} from "rxjs"; -import { SemVer } from "semver"; - -import { AuthService } from "../../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; -import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; -import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; -import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction"; -import { ServerConfig } from "../../abstractions/config/server-config"; -import { EnvironmentService, Region } from "../../abstractions/environment.service"; -import { LogService } from "../../abstractions/log.service"; -import { StateService } from "../../abstractions/state.service"; -import { ServerConfigData } from "../../models/data/server-config.data"; -import { StateProvider } from "../../state"; - -const ONE_HOUR_IN_MILLISECONDS = 1000 * 3600; - -export class ConfigService implements ConfigServiceAbstraction { - private inited = false; - - protected _serverConfig = new ReplaySubject(1); - serverConfig$ = this._serverConfig.asObservable(); - - private _forceFetchConfig = new Subject(); - protected refreshTimer$ = timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS); // after 1 hour, then every hour - - cloudRegion$ = this.serverConfig$.pipe( - map((config) => config?.environment?.cloudRegion ?? Region.US), - ); - - constructor( - private stateService: StateService, - private configApiService: ConfigApiServiceAbstraction, - private authService: AuthService, - private environmentService: EnvironmentService, - private logService: LogService, - private stateProvider: StateProvider, - - // Used to avoid duplicate subscriptions, e.g. in browser between the background and popup - private subscribe = true, - ) {} - - init() { - if (!this.subscribe || this.inited) { - return; - } - - const latestServerConfig$ = defer(() => this.configApiService.get()).pipe( - map((response) => new ServerConfigData(response)), - delayWhen((data) => this.saveConfig(data)), - catchError((e: unknown) => { - // fall back to stored ServerConfig (if any) - this.logService.error("Unable to fetch ServerConfig: " + (e as Error)?.message); - return this.stateService.getServerConfig(); - }), - ); - - // If you need to fetch a new config when an event occurs, add an observable that emits on that event here - merge( - this.refreshTimer$, // an overridable interval - this.environmentService.environment$, // when environment URLs change (including when app is started) - this._forceFetchConfig, // manual - ) - .pipe( - concatMap(() => latestServerConfig$), - map((data) => (data == null ? null : new ServerConfig(data))), - ) - .subscribe((config) => this._serverConfig.next(config)); - - this.inited = true; - } - - getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { - return this.serverConfig$.pipe( - map((serverConfig) => { - if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { - return defaultValue; - } - - return serverConfig.featureStates[key] as T; - }), - ); - } - - async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { - return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); - } - - triggerServerConfigFetch() { - this._forceFetchConfig.next(); - } - - private async saveConfig(data: ServerConfigData) { - if ((await this.authService.getAuthStatus()) === AuthenticationStatus.LoggedOut) { - return; - } - - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - await this.stateService.setServerConfig(data); - await this.environmentService.setCloudRegion(userId, data.environment?.cloudRegion); - } - - /** - * Verifies whether the server version meets the minimum required version - * @param minimumRequiredServerVersion The minimum version required - * @returns True if the server version is greater than or equal to the minimum required version - */ - checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { - return this.serverConfig$.pipe( - map((serverConfig) => { - if (serverConfig == null) { - return false; - } - const serverVersion = new SemVer(serverConfig.version); - return serverVersion.compare(minimumRequiredServerVersion) >= 0; - }), - ); - } -} diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts new file mode 100644 index 0000000000..e124deccf8 --- /dev/null +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -0,0 +1,176 @@ +import { + NEVER, + Observable, + Subject, + combineLatest, + firstValueFrom, + map, + mergeWith, + of, + shareReplay, + switchMap, + tap, +} from "rxjs"; +import { SemVer } from "semver"; + +import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; +import { UserId } from "../../../types/guid"; +import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; +import { ConfigService } from "../../abstractions/config/config.service"; +import { ServerConfig } from "../../abstractions/config/server-config"; +import { EnvironmentService, Region } from "../../abstractions/environment.service"; +import { LogService } from "../../abstractions/log.service"; +import { ServerConfigData } from "../../models/data/server-config.data"; +import { CONFIG_DISK, KeyDefinition, StateProvider, UserKeyDefinition } from "../../state"; + +export const RETRIEVAL_INTERVAL = 3_600_000; // 1 hour + +export type ApiUrl = string; + +export const USER_SERVER_CONFIG = new UserKeyDefinition(CONFIG_DISK, "serverConfig", { + deserializer: (data) => (data == null ? null : ServerConfig.fromJSON(data)), + clearOn: ["logout"], +}); + +export const GLOBAL_SERVER_CONFIGURATIONS = KeyDefinition.record( + CONFIG_DISK, + "byServer", + { + deserializer: (data) => (data == null ? null : ServerConfig.fromJSON(data)), + }, +); + +// FIXME: currently we are limited to api requests for active users. Update to accept a UserId and APIUrl once ApiService supports it. +export class DefaultConfigService implements ConfigService { + private failedFetchFallbackSubject = new Subject(); + + serverConfig$: Observable; + + cloudRegion$: Observable; + + constructor( + private configApiService: ConfigApiServiceAbstraction, + private environmentService: EnvironmentService, + private logService: LogService, + private stateProvider: StateProvider, + ) { + const apiUrl$ = this.environmentService.environment$.pipe( + map((environment) => environment.getApiUrl()), + ); + + this.serverConfig$ = combineLatest([this.stateProvider.activeUserId$, apiUrl$]).pipe( + switchMap(([userId, apiUrl]) => { + const config$ = + userId == null ? this.globalConfigFor$(apiUrl) : this.userConfigFor$(userId); + return config$.pipe(map((config) => [config, userId, apiUrl] as const)); + }), + tap(async (rec) => { + const [existingConfig, userId, apiUrl] = rec; + // Grab new config if older retrieval interval + if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) { + await this.renewConfig(existingConfig, userId, apiUrl); + } + }), + switchMap(([existingConfig]) => { + // If we needed to fetch, stop this emit, we'll get a new one after update + // This is split up with the above tap because we need to return an observable from a failed promise, + // which isn't very doable since promises are converted to observables in switchMap + if (!existingConfig || this.olderThanRetrievalInterval(existingConfig.utcDate)) { + return NEVER; + } + return of(existingConfig); + }), + // If fetch fails, we'll emit on this subject to fallback to the existing config + mergeWith(this.failedFetchFallbackSubject), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + this.cloudRegion$ = this.serverConfig$.pipe( + map((config) => config?.environment?.cloudRegion ?? Region.US), + ); + } + getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { + return this.serverConfig$.pipe( + map((serverConfig) => { + if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { + return defaultValue; + } + + return serverConfig.featureStates[key] as T; + }), + ); + } + + async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { + return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); + } + + checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { + return this.serverConfig$.pipe( + map((serverConfig) => { + if (serverConfig == null) { + return false; + } + const serverVersion = new SemVer(serverConfig.version); + return serverVersion.compare(minimumRequiredServerVersion) >= 0; + }), + ); + } + + async ensureConfigFetched() { + // Triggering a retrieval for the given user ensures that the config is less than RETRIEVAL_INTERVAL old + await firstValueFrom(this.serverConfig$); + } + + private olderThanRetrievalInterval(date: Date) { + return new Date().getTime() - date.getTime() > RETRIEVAL_INTERVAL; + } + + // Updates the on-disk configuration with a newly retrieved configuration + private async renewConfig( + existingConfig: ServerConfig, + userId: UserId, + apiUrl: string, + ): Promise { + try { + const response = await this.configApiService.get(userId); + const newConfig = new ServerConfig(new ServerConfigData(response)); + + // Update the environment region + if ( + newConfig?.environment?.cloudRegion != null && + existingConfig?.environment?.cloudRegion != newConfig.environment.cloudRegion + ) { + // Null userId sets global, otherwise sets to the given user + await this.environmentService.setCloudRegion(userId, newConfig?.environment?.cloudRegion); + } + + if (userId == null) { + // update global state with new pulled config + await this.stateProvider.getGlobal(GLOBAL_SERVER_CONFIGURATIONS).update((configs) => { + return { ...configs, [apiUrl]: newConfig }; + }); + } else { + // update state with new pulled config + await this.stateProvider.setUserState(USER_SERVER_CONFIG, newConfig, userId); + } + } catch (e) { + // mutate error to be handled by catchError + this.logService.error( + `Unable to fetch ServerConfig from ${apiUrl}: ${(e as Error)?.message}`, + ); + // Emit the existing config + this.failedFetchFallbackSubject.next(existingConfig); + } + } + + private globalConfigFor$(apiUrl: string): Observable { + return this.stateProvider + .getGlobal(GLOBAL_SERVER_CONFIGURATIONS) + .state$.pipe(map((configs) => configs?.[apiUrl])); + } + + private userConfigFor$(userId: UserId): Observable { + return this.stateProvider.getUser(userId, USER_SERVER_CONFIG).state$; + } +} diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 9160664aa5..16e6d4aa63 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -1,10 +1,10 @@ import { mock } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { firstValueFrom, of, tap } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { UserKey, MasterKey, PinKey } from "../../types/key"; @@ -18,6 +18,7 @@ import { Utils } from "../misc/utils"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { CryptoService } from "../services/crypto.service"; +import { UserKeyDefinition } from "../state"; import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state"; import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state"; @@ -40,12 +41,15 @@ describe("cryptoService", () => { const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; beforeEach(() => { accountService = mockAccountServiceWith(mockUserId); + masterPasswordService = new FakeMasterPasswordService(); stateProvider = new FakeStateProvider(accountService); cryptoService = new CryptoService( + masterPasswordService, keyGenerationService, cryptoFunctionService, encryptService, @@ -157,14 +161,14 @@ describe("cryptoService", () => { describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; - let stateSvcGetMasterKey: jest.SpyInstance; + let getMasterKey: jest.SpyInstance; beforeEach(() => { const mockRandomBytes = new Uint8Array(64) as CsprngArray; mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey; - stateSvcGetMasterKey = jest.spyOn(stateService, "getMasterKey"); + getMasterKey = jest.spyOn(masterPasswordService, "masterKey$"); }); it("returns the User Key if available", async () => { @@ -174,17 +178,17 @@ describe("cryptoService", () => { const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); expect(getKeySpy).toHaveBeenCalledWith(mockUserId); - expect(stateSvcGetMasterKey).not.toHaveBeenCalled(); + expect(getMasterKey).not.toHaveBeenCalled(); expect(userKey).toEqual(mockUserKey); }); it("returns the user's master key when User Key is not available", async () => { - stateSvcGetMasterKey.mockResolvedValue(mockMasterKey); + masterPasswordService.masterKeySubject.next(mockMasterKey); const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); - expect(stateSvcGetMasterKey).toHaveBeenCalledWith({ userId: mockUserId }); + expect(getMasterKey).toHaveBeenCalledWith(mockUserId); expect(userKey).toEqual(mockMasterKey); }); }); @@ -268,15 +272,6 @@ describe("cryptoService", () => { await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided."); }); - it("should update the user's lock state", async () => { - await cryptoService.setUserKey(mockUserKey, mockUserId); - - expect(accountService.mock.setAccountStatus).toHaveBeenCalledWith( - mockUserId, - AuthenticationStatus.Unlocked, - ); - }); - describe("Pin Key refresh", () => { let cryptoSvcMakePinKey: jest.SpyInstance; const protectedPin = @@ -336,249 +331,40 @@ describe("cryptoService", () => { }); }); - describe("clearUserKey", () => { - it.each([mockUserId, null])("should clear the User Key for id %2", async (userId) => { - await cryptoService.clearUserKey(false, userId); + describe("clearKeys", () => { + it("resolves active user id when called with no user id", async () => { + let callCount = 0; + stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++)); - expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, userId); + await cryptoService.clearKeys(null); + expect(callCount).toBe(1); + + // revert to the original state + accountService.activeAccount$ = accountService.activeAccountSubject.asObservable(); }); - it("should update status to locked", async () => { - await cryptoService.clearUserKey(false, mockUserId); + describe.each([ + USER_ENCRYPTED_ORGANIZATION_KEYS, + USER_ENCRYPTED_PROVIDER_KEYS, + USER_ENCRYPTED_PRIVATE_KEY, + USER_KEY, + ])("key removal", (key: UserKeyDefinition) => { + it(`clears ${key.key} for active user when unspecified`, async () => { + await cryptoService.clearKeys(null); - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - mockUserId, - AuthenticationStatus.Locked, - ); - }); + const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null); + }); - it.each([true, false])( - "should clear stored user keys if clearAll is true (%s)", - async (clear) => { - const clearSpy = (cryptoService["clearAllStoredUserKeys"] = jest.fn()); - await cryptoService.clearUserKey(clear, mockUserId); + it(`clears ${key.key} for the specified user when specified`, async () => { + const userId = "someOtherUser" as UserId; + await cryptoService.clearKeys(userId); - if (clear) { - expect(clearSpy).toHaveBeenCalledWith(mockUserId); - expect(clearSpy).toHaveBeenCalledTimes(1); - } else { - expect(clearSpy).not.toHaveBeenCalled(); - } - }, - ); - }); - - describe("clearOrgKeys", () => { - let forceMemorySpy: jest.Mock; - beforeEach(() => { - forceMemorySpy = cryptoService["activeUserOrgKeysState"].forceValue = jest.fn(); - }); - it("clears in memory org keys when called with memoryOnly", async () => { - await cryptoService.clearOrgKeys(true); - - expect(forceMemorySpy).toHaveBeenCalledWith({}); - }); - - it("does not clear memory when called with the non active user and memory only", async () => { - await cryptoService.clearOrgKeys(true, "someOtherUser" as UserId); - - expect(forceMemorySpy).not.toHaveBeenCalled(); - }); - - it("does not write to disk state if called with memory only", async () => { - await cryptoService.clearOrgKeys(true); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled(); - }); - - it("clears disk state when called with diskOnly", async () => { - await cryptoService.clearOrgKeys(false); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - expect( - stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_ORGANIZATION_KEYS).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("clears another user's disk state when called with diskOnly and that user", async () => { - await cryptoService.clearOrgKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - "someOtherUser" as UserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - expect( - stateProvider.singleUser.getFake( - "someOtherUser" as UserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("does not clear active user disk state when called with diskOnly and a different specified user", async () => { - await cryptoService.clearOrgKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_ORGANIZATION_KEYS, - ); - }); - }); - - describe("clearProviderKeys", () => { - let forceMemorySpy: jest.Mock; - beforeEach(() => { - forceMemorySpy = cryptoService["activeUserProviderKeysState"].forceValue = jest.fn(); - }); - it("clears in memory org keys when called with memoryOnly", async () => { - await cryptoService.clearProviderKeys(true); - - expect(forceMemorySpy).toHaveBeenCalledWith({}); - }); - - it("does not clear memory when called with the non active user and memory only", async () => { - await cryptoService.clearProviderKeys(true, "someOtherUser" as UserId); - - expect(forceMemorySpy).not.toHaveBeenCalled(); - }); - - it("does not write to disk state if called with memory only", async () => { - await cryptoService.clearProviderKeys(true); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled(); - }); - - it("clears disk state when called with diskOnly", async () => { - await cryptoService.clearProviderKeys(false); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PROVIDER_KEYS, - ); - expect( - stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PROVIDER_KEYS).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("clears another user's disk state when called with diskOnly and that user", async () => { - await cryptoService.clearProviderKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - "someOtherUser" as UserId, - USER_ENCRYPTED_PROVIDER_KEYS, - ); - expect( - stateProvider.singleUser.getFake("someOtherUser" as UserId, USER_ENCRYPTED_PROVIDER_KEYS) - .nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("does not clear active user disk state when called with diskOnly and a different specified user", async () => { - await cryptoService.clearProviderKeys(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PROVIDER_KEYS, - ); - }); - }); - - describe("clearKeyPair", () => { - let forceMemoryPrivateKeySpy: jest.Mock; - let forceMemoryPublicKeySpy: jest.Mock; - beforeEach(() => { - forceMemoryPrivateKeySpy = cryptoService["activeUserPrivateKeyState"].forceValue = jest.fn(); - forceMemoryPublicKeySpy = cryptoService["activeUserPublicKeyState"].forceValue = jest.fn(); - }); - it("clears in memory org keys when called with memoryOnly", async () => { - await cryptoService.clearKeyPair(true); - - expect(forceMemoryPrivateKeySpy).toHaveBeenCalledWith(null); - expect(forceMemoryPublicKeySpy).toHaveBeenCalledWith(null); - }); - - it("does not clear memory when called with the non active user and memory only", async () => { - await cryptoService.clearKeyPair(true, "someOtherUser" as UserId); - - expect(forceMemoryPrivateKeySpy).not.toHaveBeenCalled(); - expect(forceMemoryPublicKeySpy).not.toHaveBeenCalled(); - }); - - it("does not write to disk state if called with memory only", async () => { - await cryptoService.clearKeyPair(true); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalled(); - }); - - it("clears disk state when called with diskOnly", async () => { - await cryptoService.clearKeyPair(false); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PRIVATE_KEY, - ); - expect( - stateProvider.singleUser.getFake(mockUserId, USER_ENCRYPTED_PRIVATE_KEY).nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("clears another user's disk state when called with diskOnly and that user", async () => { - await cryptoService.clearKeyPair(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).toHaveBeenCalledWith( - "someOtherUser" as UserId, - USER_ENCRYPTED_PRIVATE_KEY, - ); - expect( - stateProvider.singleUser.getFake("someOtherUser" as UserId, USER_ENCRYPTED_PRIVATE_KEY) - .nextMock, - ).toHaveBeenCalledWith(null); - }); - - it("does not clear active user disk state when called with diskOnly and a different specified user", async () => { - await cryptoService.clearKeyPair(false, "someOtherUser" as UserId); - - expect(stateProvider.singleUser.mock.get).not.toHaveBeenCalledWith( - mockUserId, - USER_ENCRYPTED_PRIVATE_KEY, - ); - }); - }); - - describe("clearUserKey", () => { - it("clears the user key for the active user when no userId is specified", async () => { - await cryptoService.clearUserKey(false); - expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, undefined); - }); - - it("clears the user key for the specified user when a userId is specified", async () => { - await cryptoService.clearUserKey(false, "someOtherUser" as UserId); - expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(USER_KEY, null, "someOtherUser"); - }); - - it("sets the maximum account status of the active user id to locked when user id is not specified", async () => { - await cryptoService.clearUserKey(false); - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - mockUserId, - AuthenticationStatus.Locked, - ); - }); - - it("sets the maximum account status of the specified user id to locked when user id is specified", async () => { - await cryptoService.clearUserKey(false, "someOtherUser" as UserId); - expect(accountService.mock.setMaxAccountStatus).toHaveBeenCalledWith( - "someOtherUser" as UserId, - AuthenticationStatus.Locked, - ); - }); - - it("clears all stored user keys when clearAll is true", async () => { - const clearAllSpy = (cryptoService["clearAllStoredUserKeys"] = jest.fn()); - await cryptoService.clearUserKey(true); - expect(clearAllSpy).toHaveBeenCalledWith(mockUserId); + const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); + expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledWith(null); + }); }); }); }); diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index fbb6a85293..c091b6a5a9 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,7 +6,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { AccountService } from "../../auth/abstractions/account.service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; import { CsprngArray } from "../../types/csprng"; @@ -82,6 +82,7 @@ export class CryptoService implements CryptoServiceAbstraction { readonly everHadUserKey$: Observable; constructor( + protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected keyGenerationService: KeyGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected encryptService: EncryptService, @@ -144,14 +145,12 @@ export class CryptoService implements CryptoServiceAbstraction { async setUserKey(key: UserKey, userId?: UserId): Promise { if (key == null) { - throw new Error("No key provided. Use ClearUserKey to clear the key"); + throw new Error("No key provided. Lock the user to clear the key"); } // Set userId to ensure we have one for the account status update [userId, key] = await this.stateProvider.setUserState(USER_KEY, key, userId); await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, true, userId); - await this.accountService.setAccountStatus(userId, AuthenticationStatus.Unlocked); - await this.storeAdditionalKeys(key, userId); } @@ -160,6 +159,10 @@ export class CryptoService implements CryptoServiceAbstraction { await this.setUserKey(key); } + getInMemoryUserKeyFor$(userId: UserId): Observable { + return this.stateProvider.getUserState$(USER_KEY, userId); + } + async getUserKey(userId?: UserId): Promise { let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); if (userKey) { @@ -177,12 +180,16 @@ export class CryptoService implements CryptoServiceAbstraction { } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise { - return await this.validateUserKey( - (masterKey ?? (await this.getMasterKey(userId))) as unknown as UserKey, - ); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + + return await this.validateUserKey(masterKey as unknown as UserKey); } + // TODO: legacy support for user key is no longer needed since we require users to migrate on login async getUserKeyWithLegacySupport(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + const userKey = await this.getUserKey(userId); if (userKey) { return userKey; @@ -190,7 +197,8 @@ export class CryptoService implements CryptoServiceAbstraction { // Legacy support: encryption used to be done with the master key (derived from master password). // Users who have not migrated will have a null user key and must use the master key instead. - return (await this.getMasterKey(userId)) as unknown as UserKey; + const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + return masterKey as unknown as UserKey; } async getUserKeyFromStorage(keySuffix: KeySuffixOptions, userId?: UserId): Promise { @@ -229,7 +237,10 @@ export class CryptoService implements CryptoServiceAbstraction { } async makeUserKey(masterKey: MasterKey): Promise<[UserKey, EncString]> { - masterKey ||= await this.getMasterKey(); + if (!masterKey) { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + } if (masterKey == null) { throw new Error("No Master Key found."); } @@ -238,13 +249,18 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildProtectedSymmetricKey(masterKey, newUserKey.key); } - async clearUserKey(clearStoredKeys = true, userId?: UserId): Promise { - // Set userId to ensure we have one for the account status update - [userId] = await this.stateProvider.setUserState(USER_KEY, null, userId); - await this.accountService.setMaxAccountStatus(userId, AuthenticationStatus.Locked); - if (clearStoredKeys) { - await this.clearAllStoredUserKeys(userId); + /** + * Clears the user key. Clears all stored versions of the user keys as well, such as the biometrics key + * @param userId The desired user + */ + private async clearUserKey(userId: UserId): Promise { + if (userId == null) { + // nothing to do + return; } + // Set userId to ensure we have one for the account status update + await this.stateProvider.setUserState(USER_KEY, null, userId); + await this.clearAllStoredUserKeys(userId); } async clearStoredUserKey(keySuffix: KeySuffixOptions, userId?: UserId): Promise { @@ -267,28 +283,17 @@ export class CryptoService implements CryptoServiceAbstraction { } async setMasterKeyEncryptedUserKey(userKeyMasterKey: string, userId?: UserId): Promise { - await this.stateService.setMasterKeyEncryptedUserKey(userKeyMasterKey, { userId: userId }); - } - - async setMasterKey(key: MasterKey, userId?: UserId): Promise { - await this.stateService.setMasterKey(key, { userId: userId }); - } - - async getMasterKey(userId?: UserId): Promise { - let masterKey = await this.stateService.getMasterKey({ userId: userId }); - if (!masterKey) { - masterKey = (await this.stateService.getCryptoMasterKey({ userId: userId })) as MasterKey; - // if master key was null/undefined and getCryptoMasterKey also returned null/undefined, - // don't set master key as it is unnecessary - if (masterKey) { - await this.setMasterKey(masterKey, userId); - } - } - return masterKey; + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + await this.masterPasswordService.setMasterKeyEncryptedUserKey( + new EncString(userKeyMasterKey), + userId, + ); } + // TODO: Move to MasterPasswordService async getOrDeriveMasterKey(password: string, userId?: UserId) { - let masterKey = await this.getMasterKey(userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), @@ -302,6 +307,7 @@ export class CryptoService implements CryptoServiceAbstraction { * * @remarks * Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type. + * TODO: Move to MasterPasswordService */ async makeMasterKey( password: string, @@ -317,10 +323,6 @@ export class CryptoService implements CryptoServiceAbstraction { )) as MasterKey; } - async clearMasterKey(userId?: UserId): Promise { - await this.stateService.setMasterKey(null, { userId: userId }); - } - async encryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: UserKey, @@ -329,32 +331,28 @@ export class CryptoService implements CryptoServiceAbstraction { return await this.buildProtectedSymmetricKey(masterKey, userKey.key); } + // TODO: move to master password service async decryptUserKeyWithMasterKey( masterKey: MasterKey, userKey?: EncString, userId?: UserId, ): Promise { - masterKey ||= await this.getMasterKey(userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + userKey ??= await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); + masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey == null) { throw new Error("No master key found."); } - if (!userKey) { - let masterKeyEncryptedUserKey = await this.stateService.getMasterKeyEncryptedUserKey({ + // Try one more way to get the user key if it still wasn't found. + if (userKey == null) { + const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId, }); - - // Try one more way to get the user key if it still wasn't found. - if (masterKeyEncryptedUserKey == null) { - masterKeyEncryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ - userId: userId, - }); - } - - if (masterKeyEncryptedUserKey == null) { + if (deprecatedKey == null) { throw new Error("No encrypted user key found."); } - userKey = new EncString(masterKeyEncryptedUserKey); + userKey = new EncString(deprecatedKey); } let decUserKey: Uint8Array; @@ -373,12 +371,16 @@ export class CryptoService implements CryptoServiceAbstraction { return new SymmetricCryptoKey(decUserKey) as UserKey; } + // TODO: move to MasterPasswordService async hashMasterKey( password: string, key: MasterKey, hashPurpose?: HashPurpose, ): Promise { - key ||= await this.getMasterKey(); + if (!key) { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + key = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); + } if (password == null || key == null) { throw new Error("Invalid parameters."); @@ -389,20 +391,12 @@ export class CryptoService implements CryptoServiceAbstraction { return Utils.fromBufferToB64(hash); } - async setMasterKeyHash(keyHash: string): Promise { - await this.stateService.setKeyHash(keyHash); - } - - async getMasterKeyHash(): Promise { - return await this.stateService.getKeyHash(); - } - - async clearMasterKeyHash(userId?: UserId): Promise { - return await this.stateService.setKeyHash(null, { userId: userId }); - } - + // TODO: move to MasterPasswordService async compareAndUpdateKeyHash(masterPassword: string, masterKey: MasterKey): Promise { - const storedPasswordHash = await this.getMasterKeyHash(); + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const storedPasswordHash = await firstValueFrom( + this.masterPasswordService.masterKeyHash$(userId), + ); if (masterPassword != null && storedPasswordHash != null) { const localKeyHash = await this.hashMasterKey( masterPassword, @@ -420,7 +414,7 @@ export class CryptoService implements CryptoServiceAbstraction { HashPurpose.ServerAuthorization, ); if (serverKeyHash != null && storedPasswordHash === serverKeyHash) { - await this.setMasterKeyHash(localKeyHash); + await this.masterPasswordService.setMasterKeyHash(localKeyHash, userId); return true; } } @@ -476,25 +470,12 @@ export class CryptoService implements CryptoServiceAbstraction { return this.buildProtectedSymmetricKey(key, newSymKey.key); } - async clearOrgKeys(memoryOnly?: boolean, userId?: UserId): Promise { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userIdIsActive = userId == null || userId === activeUserId; - - if (!memoryOnly) { - if (userId == null && activeUserId == null) { - // nothing to do - return; - } - await this.stateProvider - .getUser(userId ?? activeUserId, USER_ENCRYPTED_ORGANIZATION_KEYS) - .update(() => null); + private async clearOrgKeys(userId: UserId): Promise { + if (userId == null) { + // nothing to do return; } - - // org keys are only cached for active users - if (userIdIsActive) { - await this.activeUserOrgKeysState.forceValue({}); - } + await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId); } async setProviderKeys(providers: ProfileProviderResponse[]): Promise { @@ -522,25 +503,12 @@ export class CryptoService implements CryptoServiceAbstraction { return await firstValueFrom(this.activeUserProviderKeys$); } - async clearProviderKeys(memoryOnly?: boolean, userId?: UserId): Promise { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userIdIsActive = userId == null || userId === activeUserId; - - if (!memoryOnly) { - if (userId == null && activeUserId == null) { - // nothing to do - return; - } - await this.stateProvider - .getUser(userId ?? activeUserId, USER_ENCRYPTED_PROVIDER_KEYS) - .update(() => null); + private async clearProviderKeys(userId: UserId): Promise { + if (userId == null) { + // nothing to do return; } - - // provider keys are only cached for active users - if (userIdIsActive) { - await this.activeUserProviderKeysState.forceValue({}); - } + await this.stateProvider.setUserState(USER_ENCRYPTED_PROVIDER_KEYS, null, userId); } async getPublicKey(): Promise { @@ -593,26 +561,17 @@ export class CryptoService implements CryptoServiceAbstraction { return [publicB64, privateEnc]; } - async clearKeyPair(memoryOnly?: boolean, userId?: UserId): Promise { - const activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const userIdIsActive = userId == null || userId === activeUserId; - - if (!memoryOnly) { - if (userId == null && activeUserId == null) { - // nothing to do - return; - } - await this.stateProvider - .getUser(userId ?? activeUserId, USER_ENCRYPTED_PRIVATE_KEY) - .update(() => null); + /** + * Clears the user's key pair + * @param userId The desired user + */ + private async clearKeyPair(userId: UserId): Promise { + if (userId == null) { + // nothing to do return; } - // decrypted key pair is only cached for active users - if (userIdIsActive) { - await this.activeUserPrivateKeyState.forceValue(null); - await this.activeUserPublicKeyState.forceValue(null); - } + await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); } async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise { @@ -677,11 +636,17 @@ export class CryptoService implements CryptoServiceAbstraction { } async clearKeys(userId?: UserId): Promise { - await this.clearUserKey(true, userId); - await this.clearMasterKeyHash(userId); - await this.clearOrgKeys(false, userId); - await this.clearProviderKeys(false, userId); - await this.clearKeyPair(false, userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + + if (userId == null) { + throw new Error("Cannot clear keys, no user Id resolved."); + } + + await this.masterPasswordService.clearMasterKeyHash(userId); + await this.clearUserKey(userId); + await this.clearOrgKeys(userId); + await this.clearProviderKeys(userId); + await this.clearKeyPair(userId); await this.clearPinKeys(userId); await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId); } @@ -1033,7 +998,8 @@ export class CryptoService implements CryptoServiceAbstraction { if (await this.isLegacyUser(masterKey, userId)) { // Legacy users don't have a user key, so no need to migrate. // Instead, set the master key for additional isLegacyUser checks that will log the user out. - await this.setMasterKey(masterKey, userId); + userId ??= await firstValueFrom(this.stateProvider.activeUserId$); + await this.masterPasswordService.setMasterKey(masterKey, userId); return; } const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ diff --git a/libs/common/src/platform/services/default-environment.service.spec.ts b/libs/common/src/platform/services/default-environment.service.spec.ts index 66bc1acfda..dd504dc302 100644 --- a/libs/common/src/platform/services/default-environment.service.spec.ts +++ b/libs/common/src/platform/services/default-environment.service.spec.ts @@ -2,14 +2,14 @@ import { firstValueFrom } from "rxjs"; import { FakeStateProvider, awaitAsync } from "../../../spec"; import { FakeAccountService } from "../../../spec/fake-account-service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { UserId } from "../../types/guid"; import { CloudRegion, Region } from "../abstractions/environment.service"; import { - ENVIRONMENT_KEY, + GLOBAL_ENVIRONMENT_KEY, DefaultEnvironmentService, EnvironmentUrls, + USER_ENVIRONMENT_KEY, } from "./default-environment.service"; // There are a few main states EnvironmentService could be in when first used @@ -31,12 +31,10 @@ describe("EnvironmentService", () => { [testUser]: { name: "name", email: "email", - status: AuthenticationStatus.Locked, }, [alternateTestUser]: { name: "name", email: "email", - status: AuthenticationStatus.Locked, }, }); stateProvider = new FakeStateProvider(accountService); @@ -49,13 +47,12 @@ describe("EnvironmentService", () => { id: userId, email: "test@example.com", name: `Test Name ${userId}`, - status: AuthenticationStatus.Unlocked, }); await awaitAsync(); }; const setGlobalData = (region: Region, environmentUrls: EnvironmentUrls) => { - stateProvider.global.getFake(ENVIRONMENT_KEY).stateSubject.next({ + stateProvider.global.getFake(GLOBAL_ENVIRONMENT_KEY).stateSubject.next({ region: region, urls: environmentUrls, }); @@ -66,7 +63,7 @@ describe("EnvironmentService", () => { environmentUrls: EnvironmentUrls, userId: UserId = testUser, ) => { - stateProvider.singleUser.getFake(userId, ENVIRONMENT_KEY).nextState({ + stateProvider.singleUser.getFake(userId, USER_ENVIRONMENT_KEY).nextState({ region: region, urls: environmentUrls, }); diff --git a/libs/common/src/platform/services/default-environment.service.ts b/libs/common/src/platform/services/default-environment.service.ts index d074ff43f8..59956ede7a 100644 --- a/libs/common/src/platform/services/default-environment.service.ts +++ b/libs/common/src/platform/services/default-environment.service.ts @@ -18,6 +18,7 @@ import { GlobalState, KeyDefinition, StateProvider, + UserKeyDefinition, } from "../state"; export class EnvironmentUrls { @@ -40,7 +41,7 @@ class EnvironmentState { } } -export const ENVIRONMENT_KEY = new KeyDefinition( +export const GLOBAL_ENVIRONMENT_KEY = new KeyDefinition( ENVIRONMENT_DISK, "environment", { @@ -48,9 +49,31 @@ export const ENVIRONMENT_KEY = new KeyDefinition( }, ); -export const CLOUD_REGION_KEY = new KeyDefinition(ENVIRONMENT_MEMORY, "cloudRegion", { - deserializer: (b) => b, -}); +export const USER_ENVIRONMENT_KEY = new UserKeyDefinition( + ENVIRONMENT_DISK, + "environment", + { + deserializer: EnvironmentState.fromJSON, + clearOn: ["logout"], + }, +); + +export const GLOBAL_CLOUD_REGION_KEY = new KeyDefinition( + ENVIRONMENT_MEMORY, + "cloudRegion", + { + deserializer: (b) => b, + }, +); + +export const USER_CLOUD_REGION_KEY = new UserKeyDefinition( + ENVIRONMENT_MEMORY, + "cloudRegion", + { + deserializer: (b) => b, + clearOn: ["logout"], + }, +); /** * The production regions available for selection. @@ -114,8 +137,8 @@ export class DefaultEnvironmentService implements EnvironmentService { private stateProvider: StateProvider, private accountService: AccountService, ) { - this.globalState = this.stateProvider.getGlobal(ENVIRONMENT_KEY); - this.globalCloudRegionState = this.stateProvider.getGlobal(CLOUD_REGION_KEY); + this.globalState = this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY); + this.globalCloudRegionState = this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY); const account$ = this.activeAccountId$.pipe( // Use == here to not trigger on undefined -> null transition @@ -125,8 +148,8 @@ export class DefaultEnvironmentService implements EnvironmentService { this.environment$ = account$.pipe( switchMap((userId) => { const t = userId - ? this.stateProvider.getUser(userId, ENVIRONMENT_KEY).state$ - : this.stateProvider.getGlobal(ENVIRONMENT_KEY).state$; + ? this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).state$ + : this.stateProvider.getGlobal(GLOBAL_ENVIRONMENT_KEY).state$; return t; }), map((state) => { @@ -136,8 +159,8 @@ export class DefaultEnvironmentService implements EnvironmentService { this.cloudWebVaultUrl$ = account$.pipe( switchMap((userId) => { const t = userId - ? this.stateProvider.getUser(userId, CLOUD_REGION_KEY).state$ - : this.stateProvider.getGlobal(CLOUD_REGION_KEY).state$; + ? this.stateProvider.getUser(userId, USER_CLOUD_REGION_KEY).state$ + : this.stateProvider.getGlobal(GLOBAL_CLOUD_REGION_KEY).state$; return t; }), map((region) => { @@ -242,7 +265,7 @@ export class DefaultEnvironmentService implements EnvironmentService { if (userId == null) { await this.globalCloudRegionState.update(() => region); } else { - await this.stateProvider.getUser(userId, CLOUD_REGION_KEY).update(() => region); + await this.stateProvider.getUser(userId, USER_CLOUD_REGION_KEY).update(() => region); } } @@ -261,13 +284,13 @@ export class DefaultEnvironmentService implements EnvironmentService { return activeUserId == null ? await firstValueFrom(this.globalState.state$) : await firstValueFrom( - this.stateProvider.getUser(userId ?? activeUserId, ENVIRONMENT_KEY).state$, + this.stateProvider.getUser(userId ?? activeUserId, USER_ENVIRONMENT_KEY).state$, ); } async seedUserEnvironment(userId: UserId) { const global = await firstValueFrom(this.globalState.state$); - await this.stateProvider.getUser(userId, ENVIRONMENT_KEY).update(() => global); + await this.stateProvider.getUser(userId, USER_ENVIRONMENT_KEY).update(() => global); } } diff --git a/libs/common/src/platform/services/key-state/org-keys.state.ts b/libs/common/src/platform/services/key-state/org-keys.state.ts index b39cc9a82a..f67e64b653 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.ts @@ -4,13 +4,14 @@ import { OrganizationId } from "../../../types/guid"; import { OrgKey } from "../../../types/key"; import { CryptoService } from "../../abstractions/crypto.service"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } from "../../state"; +import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; -export const USER_ENCRYPTED_ORGANIZATION_KEYS = KeyDefinition.record< +export const USER_ENCRYPTED_ORGANIZATION_KEYS = UserKeyDefinition.record< EncryptedOrganizationKeyData, OrganizationId >(CRYPTO_DISK, "organizationKeys", { deserializer: (obj) => obj, + clearOn: ["logout"], }); export const USER_ORGANIZATION_KEYS = DeriveDefinition.from< diff --git a/libs/common/src/platform/services/key-state/provider-keys.state.ts b/libs/common/src/platform/services/key-state/provider-keys.state.ts index c89df34c80..776fdc77d8 100644 --- a/libs/common/src/platform/services/key-state/provider-keys.state.ts +++ b/libs/common/src/platform/services/key-state/provider-keys.state.ts @@ -3,14 +3,15 @@ import { ProviderKey } from "../../../types/key"; import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { KeyDefinition, CRYPTO_DISK, DeriveDefinition } from "../../state"; +import { CRYPTO_DISK, DeriveDefinition, UserKeyDefinition } from "../../state"; import { CryptoService } from "../crypto.service"; -export const USER_ENCRYPTED_PROVIDER_KEYS = KeyDefinition.record( +export const USER_ENCRYPTED_PROVIDER_KEYS = UserKeyDefinition.record( CRYPTO_DISK, "providerKeys", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); diff --git a/libs/common/src/platform/services/key-state/user-key.state.ts b/libs/common/src/platform/services/key-state/user-key.state.ts index d0f54c9add..609525b0ac 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.ts @@ -3,18 +3,24 @@ import { CryptoFunctionService } from "../../abstractions/crypto-function.servic import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; -import { KeyDefinition, CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY } from "../../state"; +import { CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; import { CryptoService } from "../crypto.service"; -export const USER_EVER_HAD_USER_KEY = new KeyDefinition(CRYPTO_DISK, "everHadUserKey", { - deserializer: (obj) => obj, -}); +export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition( + CRYPTO_DISK, + "everHadUserKey", + { + deserializer: (obj) => obj, + clearOn: ["logout"], + }, +); -export const USER_ENCRYPTED_PRIVATE_KEY = new KeyDefinition( +export const USER_ENCRYPTED_PRIVATE_KEY = new UserKeyDefinition( CRYPTO_DISK, "privateKey", { deserializer: (obj) => obj, + clearOn: ["logout"], }, ); @@ -58,6 +64,7 @@ export const USER_PUBLIC_KEY = DeriveDefinition.from< return (await cryptoFunctionService.rsaExtractPublicKey(privateKey)) as UserPublicKey; }, }); -export const USER_KEY = new KeyDefinition(CRYPTO_MEMORY, "userKey", { +export const USER_KEY = new UserKeyDefinition(CRYPTO_MEMORY, "userKey", { deserializer: (obj) => SymmetricCryptoKey.fromJSON(obj) as UserKey, + clearOn: ["logout", "lock"], }); diff --git a/libs/common/src/platform/services/migration-builder.service.spec.ts b/libs/common/src/platform/services/migration-builder.service.spec.ts index c06d4bf53c..1330ea07a4 100644 --- a/libs/common/src/platform/services/migration-builder.service.spec.ts +++ b/libs/common/src/platform/services/migration-builder.service.spec.ts @@ -82,6 +82,7 @@ describe("MigrationBuilderService", () => { startingStateVersion, new FakeStorageService(startingState), mock(), + "general", ); await sut.build().migrate(helper); diff --git a/libs/common/src/platform/services/migration-runner.ts b/libs/common/src/platform/services/migration-runner.ts index f900343424..006031f7e5 100644 --- a/libs/common/src/platform/services/migration-runner.ts +++ b/libs/common/src/platform/services/migration-runner.ts @@ -18,6 +18,7 @@ export class MigrationRunner { await currentVersion(this.diskStorage, this.logService), this.diskStorage, this.logService, + "general", ); if (migrationHelper.currentVersion < 0) { diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 6ff1c63b50..9edc9ed1e3 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1,24 +1,14 @@ -import { BehaviorSubject, Observable, map } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; -import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable"; -import { ForceSetPasswordReason } from "../../auth/models/domain/force-set-password-reason"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; -import { SendData } from "../../tools/send/models/data/send.data"; -import { SendView } from "../../tools/send/models/view/send.view"; import { UserId } from "../../types/guid"; -import { DeviceKey, MasterKey } from "../../types/key"; -import { CipherData } from "../../vault/models/data/cipher.data"; -import { LocalData } from "../../vault/models/data/local.data"; -import { CipherView } from "../../vault/models/view/cipher.view"; -import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { EnvironmentService } from "../abstractions/environment.service"; import { LogService } from "../abstractions/log.service"; import { @@ -32,13 +22,11 @@ import { import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; -import { ServerConfigData } from "../models/data/server-config.data"; import { Account, AccountData, AccountSettings } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { GlobalState } from "../models/domain/global-state"; import { State } from "../models/domain/state"; import { StorageOptions } from "../models/domain/storage-options"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { MigrationRunner } from "./migration-runner"; @@ -74,8 +62,6 @@ export class StateService< protected activeAccountSubject = new BehaviorSubject(null); activeAccount$ = this.activeAccountSubject.asObservable(); - activeAccountUnlocked$: Observable; - private hasBeenInited = false; protected isRecoveredSession = false; @@ -95,13 +81,7 @@ export class StateService< protected tokenService: TokenService, private migrationRunner: MigrationRunner, protected useAccountCache: boolean = true, - ) { - this.activeAccountUnlocked$ = this.accountService.activeAccount$.pipe( - map((a) => { - return a?.status === AuthenticationStatus.Unlocked; - }), - ); - } + ) {} async init(initOptions: InitOptions = {}): Promise { // Deconstruct and apply defaults @@ -157,7 +137,6 @@ export class StateService< await this.accountService.addAccount(state.activeUserId as UserId, { name: activeDiskAccount.profile.name, email: activeDiskAccount.profile.email, - status: AuthenticationStatus.LoggedOut, }); } await this.accountService.switchAccount(state.activeUserId as UserId); @@ -183,16 +162,7 @@ export class StateService< // TODO: Temporary update to avoid routing all account status changes through account service for now. // The determination of state should be handled by the various services that control those values. - const token = await this.tokenService.getAccessToken(userId as UserId); - const autoKey = await this.getUserKeyAutoUnlock({ userId: userId }); - const accountStatus = - token == null - ? AuthenticationStatus.LoggedOut - : autoKey == null - ? AuthenticationStatus.Locked - : AuthenticationStatus.Unlocked; await this.accountService.addAccount(userId as UserId, { - status: accountStatus, name: diskAccount.profile.name, email: diskAccount.profile.email, }); @@ -212,7 +182,6 @@ export class StateService< await this.setLastActive(new Date().getTime(), { userId: account.profile.userId }); // TODO: Temporary update to avoid routing all account status changes through account service for now. await this.accountService.addAccount(account.profile.userId as UserId, { - status: AuthenticationStatus.Locked, name: account.profile.name, email: account.profile.email, }); @@ -248,128 +217,6 @@ export class StateService< return currentUser as UserId; } - async getAddEditCipherInfo(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - // ensure prototype on cipher - const raw = account?.data?.addEditCipherInfo; - return raw == null - ? null - : { - cipher: - raw?.cipher.toJSON != null - ? raw.cipher - : CipherView.fromJSON(raw?.cipher as Jsonify), - collectionIds: raw?.collectionIds, - }; - } - - async setAddEditCipherInfo(value: AddEditCipherInfo, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.data.addEditCipherInfo = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - async getBiometricFingerprintValidated(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.biometricFingerprintValidated ?? false - ); - } - - async setBiometricFingerprintValidated(value: boolean, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.biometricFingerprintValidated = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getConvertAccountToKeyConnector(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.convertAccountToKeyConnector; - } - - async setConvertAccountToKeyConnector(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.convertAccountToKeyConnector = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - /** - * @deprecated Do not save the Master Key. Use the User Symmetric Key instead - */ - async getCryptoMasterKey(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - return account?.keys?.cryptoMasterKey; - } - - /** - * User's master key derived from MP, saved only if we decrypted with MP - */ - async getMasterKey(options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - return account?.keys?.masterKey; - } - - /** - * User's master key derived from MP, saved only if we decrypted with MP - */ - async setMasterKey(value: MasterKey, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.keys.masterKey = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - /** - * The master key encrypted User symmetric key, saved on every auth - * so we can unlock with MP offline - */ - async getMasterKeyEncryptedUserKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.keys.masterKeyEncryptedUserKey; - } - - /** - * The master key encrypted User symmetric key, saved on every auth - * so we can unlock with MP offline - */ - async setMasterKeyEncryptedUserKey(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.keys.masterKeyEncryptedUserKey = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - /** * user key when using the "never" option of vault timeout */ @@ -586,24 +433,6 @@ export class StateService< await this.saveSecureStorageKey(partialKeys.biometricKey, value, options); } - @withPrototypeForArrayMembers(CipherView, CipherView.fromJSON) - async getDecryptedCiphers(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.data?.ciphers?.decrypted; - } - - async setDecryptedCiphers(value: CipherView[], options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.data.ciphers.decrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - @withPrototypeForArrayMembers(GeneratedPasswordHistory) async getDecryptedPasswordGenerationHistory( options?: StorageOptions, @@ -650,42 +479,6 @@ export class StateService< ); } - @withPrototypeForArrayMembers(SendView) - async getDecryptedSends(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.data?.sends?.decrypted; - } - - async setDecryptedSends(value: SendView[], options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.data.sends.decrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - - async getDisableGa(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableGa ?? false - ); - } - - async setDisableGa(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.disableGa = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getDuckDuckGoSharedKey(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); if (options?.userId == null) { @@ -704,95 +497,6 @@ export class StateService< : await this.secureStorageService.save(DDG_SHARED_KEY, value, options); } - async getDeviceKey(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return null; - } - - const account = await this.getAccount(options); - - const existingDeviceKey = account?.keys?.deviceKey; - - // Must manually instantiate the SymmetricCryptoKey class from the JSON object - if (existingDeviceKey != null) { - return SymmetricCryptoKey.fromJSON(existingDeviceKey) as DeviceKey; - } else { - return null; - } - } - - async setDeviceKey(value: DeviceKey | null, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return; - } - - const account = await this.getAccount(options); - - account.keys.deviceKey = value?.toJSON() ?? null; - - await this.saveAccount(account, options); - } - - async getAdminAuthRequest(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return null; - } - - const account = await this.getAccount(options); - - return account?.adminAuthRequest - ? AdminAuthRequestStorable.fromJSON(account.adminAuthRequest) - : null; - } - - async setAdminAuthRequest( - adminAuthRequest: AdminAuthRequestStorable, - options?: StorageOptions, - ): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return; - } - - const account = await this.getAccount(options); - - account.adminAuthRequest = adminAuthRequest?.toJSON(); - - await this.saveAccount(account, options); - } - - async getShouldTrustDevice(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return null; - } - - const account = await this.getAccount(options); - - return account?.settings?.trustDeviceChoiceForDecryption ?? null; - } - - async setShouldTrustDevice(value: boolean, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - if (options?.userId == null) { - return; - } - - const account = await this.getAccount(options); - - account.settings.trustDeviceChoiceForDecryption = value; - - await this.saveAccount(account, options); - } - async getEmail(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -867,48 +571,6 @@ export class StateService< ); } - async getEnableDuckDuckGoBrowserIntegration(options?: StorageOptions): Promise { - return ( - (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.enableDuckDuckGoBrowserIntegration ?? false - ); - } - - async setEnableDuckDuckGoBrowserIntegration( - value: boolean, - options?: StorageOptions, - ): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.enableDuckDuckGoBrowserIntegration = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - @withPrototypeForObjectValues(CipherData) - async getEncryptedCiphers(options?: StorageOptions): Promise<{ [id: string]: CipherData }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) - )?.data?.ciphers?.encrypted; - } - - async setEncryptedCiphers( - value: { [id: string]: CipherData }, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.data.ciphers.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - /** * @deprecated Use UserKey instead */ @@ -958,27 +620,6 @@ export class StateService< ); } - @withPrototypeForObjectValues(SendData) - async getEncryptedSends(options?: StorageOptions): Promise<{ [id: string]: SendData }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) - )?.data?.sends.encrypted; - } - - async setEncryptedSends( - value: { [id: string]: SendData }, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.data.sends.encrypted = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - async getEverBeenUnlocked(options?: StorageOptions): Promise { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))) @@ -997,47 +638,6 @@ export class StateService< ); } - async getForceSetPasswordReason(options?: StorageOptions): Promise { - return ( - ( - await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ) - )?.profile?.forceSetPasswordReason ?? ForceSetPasswordReason.None - ); - } - - async setForceSetPasswordReason( - value: ForceSetPasswordReason, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - account.profile.forceSetPasswordReason = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()), - ); - } - - async getInstalledVersion(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.installedVersion; - } - - async setInstalledVersion(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.installedVersion = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getIsAuthenticated(options?: StorageOptions): Promise { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && @@ -1088,23 +688,6 @@ export class StateService< ); } - async getKeyHash(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.keyHash; - } - - async setKeyHash(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.keyHash = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getLastActive(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); @@ -1151,26 +734,6 @@ export class StateService< ); } - async getLocalData(options?: StorageOptions): Promise<{ [cipherId: string]: LocalData }> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.data?.localData; - } - - async setLocalData( - value: { [cipherId: string]: LocalData }, - options?: StorageOptions, - ): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.data.localData = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - async getMinimizeOnCopyToClipboard(options?: StorageOptions): Promise { return ( (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1280,23 +843,6 @@ export class StateService< ); } - async getRememberedEmail(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.rememberedEmail; - } - - async setRememberedEmail(value: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - globals.rememberedEmail = value; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - async getSecurityStamp(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -1320,23 +866,6 @@ export class StateService< )?.profile?.userId; } - async getUsesKeyConnector(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.usesKeyConnector; - } - - async setUsesKeyConnector(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.usesKeyConnector = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getVaultTimeout(options?: StorageOptions): Promise { const accountVaultTimeout = ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) @@ -1380,58 +909,6 @@ export class StateService< ); } - async getApproveLoginRequests(options?: StorageOptions): Promise { - const approveLoginRequests = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.approveLoginRequests; - return approveLoginRequests; - } - - async setApproveLoginRequests(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.approveLoginRequests = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async setServerConfig(value: ServerConfigData, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - account.settings.serverConfig = value; - return await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()), - ); - } - - async getServerConfig(options: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) - )?.settings?.serverConfig; - } - - async getDeepLinkRedirectUrl(options?: StorageOptions): Promise { - return ( - await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.deepLinkRedirectUrl; - } - - async setDeepLinkRedirectUrl(url: string, options?: StorageOptions): Promise { - const globals = await this.getGlobals( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - globals.deepLinkRedirectUrl = url; - await this.saveGlobals( - globals, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - protected async getGlobals(options: StorageOptions): Promise { let globals: TGlobalState; if (this.useMemory(options.storageLocation)) { @@ -1768,16 +1245,12 @@ export class StateService< return state; }); - // TODO: Invert this logic, we should remove accounts based on logged out emit - await this.accountService.setAccountStatus(userId as UserId, AuthenticationStatus.LoggedOut); } // settings persist even on reset, and are not affected by this method protected resetAccount(account: TAccount) { const persistentAccountInformation = { settings: account.settings, - keys: { deviceKey: account.keys.deviceKey }, - adminAuthRequest: account.adminAuthRequest, }; return Object.assign(this.createAccount(), persistentAccountInformation); } @@ -1802,7 +1275,9 @@ export class StateService< } protected async deAuthenticateAccount(userId: string): Promise { - await this.tokenService.clearAccessToken(userId as UserId); + // We must have a manual call to clear tokens as we can't leverage state provider to clean + // up our data as we have secure storage in the mix. + await this.tokenService.clearTokens(userId as UserId); await this.setLastActive(null, { userId: userId }); await this.updateState(async (state) => { state.authenticatedAccounts = state.authenticatedAccounts.filter((id) => id !== userId); @@ -1944,50 +1419,3 @@ function withPrototypeForArrayMembers( }; }; } - -function withPrototypeForObjectValues( - valuesConstructor: new (...args: any[]) => T, - valuesConverter: (input: any) => T = (i) => i, -): ( - target: any, - propertyKey: string | symbol, - descriptor: PropertyDescriptor, -) => { value: (...args: any[]) => Promise<{ [key: string]: T }> } { - return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor.value; - - return { - value: function (...args: any[]) { - const originalResult: Promise<{ [key: string]: T }> = originalMethod.apply(this, args); - - if (!Utils.isPromise(originalResult)) { - throw new Error( - `Error applying prototype to stored value -- result is not a promise for method ${String( - propertyKey, - )}`, - ); - } - - return originalResult.then((result) => { - if (result == null) { - return null; - } else { - for (const [key, val] of Object.entries(result)) { - result[key] = - val == null || val.constructor.name === valuesConstructor.prototype.constructor.name - ? valuesConverter(val) - : valuesConverter( - Object.create( - valuesConstructor.prototype, - Object.getOwnPropertyDescriptors(val), - ), - ); - } - - return result as { [key: string]: T }; - } - }); - }, - }; - }; -} diff --git a/libs/common/src/platform/services/translation.service.ts b/libs/common/src/platform/services/translation.service.ts index aa41073878..4ad8162af5 100644 --- a/libs/common/src/platform/services/translation.service.ts +++ b/libs/common/src/platform/services/translation.service.ts @@ -16,6 +16,7 @@ export abstract class TranslationService implements TranslationServiceAbstractio ["bs", "bosanski jezik"], ["ca", "català"], ["cs", "čeština"], + ["cy", "Cymraeg, y Gymraeg"], ["da", "dansk"], ["de", "Deutsch"], ["el", "Ελληνικά"], @@ -30,6 +31,7 @@ export abstract class TranslationService implements TranslationServiceAbstractio ["fi", "suomi"], ["fil", "Wikang Filipino"], ["fr", "français"], + ["gl", "galego"], ["he", "עברית"], ["hi", "हिन्दी"], ["hr", "hrvatski"], @@ -45,9 +47,13 @@ export abstract class TranslationService implements TranslationServiceAbstractio ["lv", "Latvietis"], ["me", "црногорски"], ["ml", "മലയാളം"], + ["mr", "मराठी"], + ["my", "ဗမာစကား"], ["nb", "norsk (bokmål)"], + ["ne", "नेपाली"], ["nl", "Nederlands"], ["nn", "Norsk Nynorsk"], + ["or", "ଓଡ଼ିଆ"], ["pl", "polski"], ["pt-BR", "português do Brasil"], ["pt-PT", "português"], @@ -58,6 +64,7 @@ export abstract class TranslationService implements TranslationServiceAbstractio ["sl", "Slovenski jezik, Slovenščina"], ["sr", "Српски"], ["sv", "svenska"], + ["te", "తెలుగు"], ["th", "ไทย"], ["tr", "Türkçe"], ["uk", "українська"], diff --git a/libs/common/src/platform/state/derive-definition.ts b/libs/common/src/platform/state/derive-definition.ts index 6c514f8869..8f62d3a342 100644 --- a/libs/common/src/platform/state/derive-definition.ts +++ b/libs/common/src/platform/state/derive-definition.ts @@ -5,6 +5,7 @@ import { DerivedStateDependencies, StorageKey } from "../../types/state"; import { KeyDefinition } from "./key-definition"; import { StateDefinition } from "./state-definition"; +import { UserKeyDefinition } from "./user-key-definition"; declare const depShapeMarker: unique symbol; /** @@ -129,26 +130,28 @@ export class DeriveDefinition( definition: | KeyDefinition + | UserKeyDefinition | [DeriveDefinition, string], options: DeriveDefinitionOptions, ) { - if (isKeyDefinition(definition)) { - return new DeriveDefinition(definition.stateDefinition, definition.key, options); - } else { + if (isFromDeriveDefinition(definition)) { return new DeriveDefinition(definition[0].stateDefinition, definition[1], options); + } else { + return new DeriveDefinition(definition.stateDefinition, definition.key, options); } } static fromWithUserId( definition: | KeyDefinition + | UserKeyDefinition | [DeriveDefinition, string], options: DeriveDefinitionOptions<[UserId, TKeyDef], TTo, TDeps>, ) { - if (isKeyDefinition(definition)) { - return new DeriveDefinition(definition.stateDefinition, definition.key, options); - } else { + if (isFromDeriveDefinition(definition)) { return new DeriveDefinition(definition[0].stateDefinition, definition[1], options); + } else { + return new DeriveDefinition(definition.stateDefinition, definition.key, options); } } @@ -181,10 +184,11 @@ export class DeriveDefinition + | UserKeyDefinition | [DeriveDefinition, string], -): definition is KeyDefinition { - return Object.prototype.hasOwnProperty.call(definition, "key"); +): definition is [DeriveDefinition, string] { + return Array.isArray(definition); } diff --git a/libs/common/src/platform/state/derived-state.provider.ts b/libs/common/src/platform/state/derived-state.provider.ts index cf0a0c56c7..2186048247 100644 --- a/libs/common/src/platform/state/derived-state.provider.ts +++ b/libs/common/src/platform/state/derived-state.provider.ts @@ -17,9 +17,9 @@ export abstract class DerivedStateProvider { * well as some memory persistent information. * @param dependencies The dependencies of the derive function */ - get: ( + abstract get( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, - ) => DerivedState; + ): DerivedState; } diff --git a/libs/common/src/platform/state/global-state.provider.ts b/libs/common/src/platform/state/global-state.provider.ts index 7c791b6b4d..5aa2b26a5b 100644 --- a/libs/common/src/platform/state/global-state.provider.ts +++ b/libs/common/src/platform/state/global-state.provider.ts @@ -9,5 +9,5 @@ export abstract class GlobalStateProvider { * Gets a {@link GlobalState} scoped to the given {@link KeyDefinition} * @param keyDefinition - The {@link KeyDefinition} for which you want the state for. */ - get: (keyDefinition: KeyDefinition) => GlobalState; + abstract get(keyDefinition: KeyDefinition): GlobalState; } diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts index 6e01b615d7..51a972a9dc 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts @@ -9,7 +9,6 @@ import { Jsonify } from "type-fest"; import { awaitAsync, trackEmissions } from "../../../../spec"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { AccountInfo } from "../../../auth/abstractions/account.service"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { StorageServiceProvider } from "../../services/storage-service.provider"; import { StateDefinition } from "../state-definition"; @@ -84,7 +83,6 @@ describe("DefaultActiveUserState", () => { id: userId, email: `test${id}@example.com`, name: `Test User ${id}`, - status: AuthenticationStatus.Unlocked, }); await awaitAsync(); }; diff --git a/libs/common/src/platform/state/index.ts b/libs/common/src/platform/state/index.ts index dd14aaf329..367beefb49 100644 --- a/libs/common/src/platform/state/index.ts +++ b/libs/common/src/platform/state/index.ts @@ -8,7 +8,7 @@ export { ActiveUserState, SingleUserState, CombinedState } from "./user-state"; export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider"; export { KeyDefinition, KeyDefinitionOptions } from "./key-definition"; export { StateUpdateOptions } from "./state-update-options"; -export { UserKeyDefinition } from "./user-key-definition"; +export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition"; export { StateEventRunnerService } from "./state-event-runner.service"; export * from "./state-definitions"; diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index bf0d162eee..18df252062 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -35,15 +35,28 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); // Auth +export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); +export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); +export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk"); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); +export const ROUTER_DISK = new StateDefinition("router", "disk"); +export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { + web: "disk-local", +}); +export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); +export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", { + web: "disk-local", +}); export const SSO_DISK = new StateDefinition("ssoLogin", "disk"); export const TOKEN_DISK = new StateDefinition("token", "disk"); export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { web: "disk-local", }); export const TOKEN_MEMORY = new StateDefinition("token", "memory"); -export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); +export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", { + web: "disk-local", +}); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); // Autofill @@ -66,6 +79,10 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne web: "disk-local", }); +export const UNASSIGNED_ITEMS_BANNER_DISK = new StateDefinition("unassignedItemsBanner", "disk", { + web: "disk-local", +}); + // Platform export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", { @@ -73,6 +90,9 @@ export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", }); export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk"); export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk"); +export const CONFIG_DISK = new StateDefinition("config", "disk", { + web: "disk-local", +}); export const CRYPTO_DISK = new StateDefinition("crypto", "disk"); export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory"); export const DESKTOP_SETTINGS_DISK = new StateDefinition("desktopSettings", "disk"); @@ -91,7 +111,12 @@ export const SM_ONBOARDING_DISK = new StateDefinition("smOnboarding", "disk", { export const GENERATOR_DISK = new StateDefinition("generator", "disk"); export const GENERATOR_MEMORY = new StateDefinition("generator", "memory"); +export const BROWSER_SEND_MEMORY = new StateDefinition("sendBrowser", "memory"); export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "disk"); +export const SEND_DISK = new StateDefinition("encryptedSend", "disk", { + web: "memory", +}); +export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory"); // Vault @@ -108,3 +133,10 @@ export const VAULT_ONBOARDING = new StateDefinition("vaultOnboarding", "disk", { export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", { web: "disk-local", }); +export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory"); +export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory"); +export const CIPHERS_DISK = new StateDefinition("ciphers", "disk", { web: "memory" }); +export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", { + web: "disk-local", +}); +export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory"); diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts index ddbb6a7c87..a1e51552c7 100644 --- a/libs/common/src/platform/state/state.provider.ts +++ b/libs/common/src/platform/state/state.provider.ts @@ -19,7 +19,7 @@ import { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.p */ export abstract class StateProvider { /** @see{@link ActiveUserStateProvider.activeUserId$} */ - activeUserId$: Observable; + abstract activeUserId$: Observable; /** * Gets a state observable for a given key and userId. @@ -149,10 +149,10 @@ export abstract class StateProvider { ): SingleUserState; /** @see{@link GlobalStateProvider.get} */ - getGlobal: (keyDefinition: KeyDefinition) => GlobalState; - getDerived: ( + abstract getGlobal(keyDefinition: KeyDefinition): GlobalState; + abstract getDerived( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, - ) => DerivedState; + ): DerivedState; } diff --git a/libs/common/src/platform/state/user-key-definition.ts b/libs/common/src/platform/state/user-key-definition.ts index 3405b38837..3eb9369080 100644 --- a/libs/common/src/platform/state/user-key-definition.ts +++ b/libs/common/src/platform/state/user-key-definition.ts @@ -8,7 +8,7 @@ import { StateDefinition } from "./state-definition"; export type ClearEvent = "lock" | "logout"; -type UserKeyDefinitionOptions = KeyDefinitionOptions & { +export type UserKeyDefinitionOptions = KeyDefinitionOptions & { clearOn: ClearEvent[]; }; diff --git a/libs/common/src/platform/state/user-state.provider.ts b/libs/common/src/platform/state/user-state.provider.ts index 2f18f3678d..3af10218f8 100644 --- a/libs/common/src/platform/state/user-state.provider.ts +++ b/libs/common/src/platform/state/user-state.provider.ts @@ -39,7 +39,7 @@ export abstract class ActiveUserStateProvider { /** * Convenience re-emission of active user ID from {@link AccountService.activeAccount$} */ - activeUserId$: Observable; + abstract activeUserId$: Observable; /** * Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such diff --git a/libs/common/src/platform/state/user-state.ts b/libs/common/src/platform/state/user-state.ts index dc994cf9fd..44bc873254 100644 --- a/libs/common/src/platform/state/user-state.ts +++ b/libs/common/src/platform/state/user-state.ts @@ -6,24 +6,25 @@ import { StateUpdateOptions } from "./state-update-options"; export type CombinedState = readonly [userId: UserId, state: T]; -/** - * A helper object for interacting with state that is scoped to a specific user. - */ +/** A helper object for interacting with state that is scoped to a specific user. */ export interface UserState { - /** - * Emits a stream of data. - */ - readonly state$: Observable; + /** Emits a stream of data. Emits null if the user does not have specified state. */ + readonly state$: Observable; - /** - * Emits a stream of data alongside the user id the data corresponds to. - */ + /** Emits a stream of tuples, with the first element being a user id and the second element being the data for that user. */ readonly combinedState$: Observable>; } export const activeMarker: unique symbol = Symbol("active"); export interface ActiveUserState extends UserState { readonly [activeMarker]: true; + + /** + * Emits a stream of data. Emits null if the user does not have specified state. + * Note: Will not emit if there is no active user. + */ + readonly state$: Observable; + /** * Updates backing stores for the active user. * @param configureState function that takes the current state and returns the new state diff --git a/libs/common/src/platform/theming/theme-state.service.ts b/libs/common/src/platform/theming/theme-state.service.ts index 42b5b1770c..9c31733416 100644 --- a/libs/common/src/platform/theming/theme-state.service.ts +++ b/libs/common/src/platform/theming/theme-state.service.ts @@ -7,13 +7,13 @@ export abstract class ThemeStateService { /** * The users selected theme. */ - selectedTheme$: Observable; + abstract selectedTheme$: Observable; /** * A method for updating the current users configured theme. * @param theme The chosen user theme. */ - setSelectedTheme: (theme: ThemeType) => Promise; + abstract setSelectedTheme(theme: ThemeType): Promise; } const THEME_SELECTION = new KeyDefinition(THEMING_DISK, "selection", { diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index b6c2ab5c22..e8135f3d6c 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -7,8 +7,6 @@ import { OrganizationSponsorshipRedeemRequest } from "../admin-console/models/re import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request"; import { ProviderAddOrganizationRequest } from "../admin-console/models/request/provider/provider-add-organization.request"; import { ProviderOrganizationCreateRequest } from "../admin-console/models/request/provider/provider-organization-create.request"; -import { ProviderSetupRequest } from "../admin-console/models/request/provider/provider-setup.request"; -import { ProviderUpdateRequest } from "../admin-console/models/request/provider/provider-update.request"; import { ProviderUserAcceptRequest } from "../admin-console/models/request/provider/provider-user-accept.request"; import { ProviderUserBulkConfirmRequest } from "../admin-console/models/request/provider/provider-user-bulk-confirm.request"; import { ProviderUserBulkRequest } from "../admin-console/models/request/provider/provider-user-bulk.request"; @@ -32,7 +30,6 @@ import { ProviderUserResponse, ProviderUserUserDetailsResponse, } from "../admin-console/models/response/provider/provider-user.response"; -import { ProviderResponse } from "../admin-console/models/response/provider/provider.response"; import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response"; import { TokenService } from "../auth/abstractions/token.service"; import { CreateAuthRequest } from "../auth/models/request/create-auth.request"; @@ -565,8 +562,12 @@ export class ApiService implements ApiServiceAbstraction { return this.send("PUT", "/ciphers/share", request, true, false); } - putCipherCollections(id: string, request: CipherCollectionsRequest): Promise { - return this.send("PUT", "/ciphers/" + id + "/collections", request, true, false); + async putCipherCollections( + id: string, + request: CipherCollectionsRequest, + ): Promise { + const response = await this.send("PUT", "/ciphers/" + id + "/collections", request, true, true); + return new CipherResponse(response); } putCipherCollectionsAdmin(id: string, request: CipherCollectionsRequest): Promise { @@ -862,16 +863,6 @@ export class ApiService implements ApiServiceAbstraction { return r; } - async putGroupUsers(organizationId: string, id: string, request: string[]): Promise { - await this.send( - "PUT", - "/organizations/" + organizationId + "/groups/" + id + "/users", - request, - true, - false, - ); - } - deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise { return this.send( "DELETE", @@ -1157,23 +1148,6 @@ export class ApiService implements ApiServiceAbstraction { return this.send("DELETE", "/organizations/connections/" + id, null, true, false); } - // Provider APIs - - async postProviderSetup(id: string, request: ProviderSetupRequest) { - const r = await this.send("POST", "/providers/" + id + "/setup", request, true, true); - return new ProviderResponse(r); - } - - async getProvider(id: string) { - const r = await this.send("GET", "/providers/" + id, null, true, true); - return new ProviderResponse(r); - } - - async putProvider(id: string, request: ProviderUpdateRequest) { - const r = await this.send("PUT", "/providers/" + id, request, true, true); - return new ProviderResponse(r); - } - // Provider User APIs async getProviderUsers( @@ -1780,9 +1754,9 @@ export class ApiService implements ApiServiceAbstraction { await this.tokenService.setTokens( tokenResponse.accessToken, - tokenResponse.refreshToken, vaultTimeoutAction as VaultTimeoutAction, vaultTimeout, + tokenResponse.refreshToken, ); } else { const error = await this.handleError(response, true, true); diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index 2d2b553062..641c1b4d44 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -3,7 +3,7 @@ import { firstValueFrom, map, from, zip } from "rxjs"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; -import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventType } from "../../enums"; import { EventData } from "../../models/data/event.data"; @@ -18,7 +18,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private stateProvider: StateProvider, private organizationService: OrganizationService, private eventUploadService: EventUploadService, - private accountService: AccountService, + private authService: AuthService, ) {} /** Adds an event to the active user's event collection @@ -71,12 +71,12 @@ export class EventCollectionService implements EventCollectionServiceAbstraction const cipher$ = from(this.cipherService.get(cipherId)); - const [accountInfo, orgIds, cipher] = await firstValueFrom( - zip(this.accountService.activeAccount$, orgIds$, cipher$), + const [authStatus, orgIds, cipher] = await firstValueFrom( + zip(this.authService.activeAccountStatus$, orgIds$, cipher$), ); // The user must be authorized - if (accountInfo.status != AuthenticationStatus.Unlocked) { + if (authStatus != AuthenticationStatus.Unlocked) { return false; } diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index 4ee4300c39..6f229751bf 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -2,7 +2,7 @@ import { firstValueFrom, map } from "rxjs"; import { ApiService } from "../../abstractions/api.service"; import { EventUploadService as EventUploadServiceAbstraction } from "../../abstractions/event/event-upload.service"; -import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { EventData } from "../../models/data/event.data"; import { EventRequest } from "../../models/request/event.request"; @@ -18,7 +18,7 @@ export class EventUploadService implements EventUploadServiceAbstraction { private apiService: ApiService, private stateProvider: StateProvider, private logService: LogService, - private accountService: AccountService, + private authService: AuthService, ) {} init(checkOnInterval: boolean) { @@ -43,13 +43,16 @@ export class EventUploadService implements EventUploadServiceAbstraction { userId = await firstValueFrom(this.stateProvider.activeUserId$); } - // Get the auth status from the provided user or the active user - const userAuth$ = this.accountService.accounts$.pipe( - map((accounts) => accounts[userId]?.status === AuthenticationStatus.Unlocked), - ); + if (!userId) { + return; + } - const isAuthenticated = await firstValueFrom(userAuth$); - if (!isAuthenticated) { + const isUnlocked = await firstValueFrom( + this.authService + .authStatusFor$(userId) + .pipe(map((status) => status === AuthenticationStatus.Unlocked)), + ); + if (!isUnlocked) { return; } diff --git a/libs/common/src/services/event/key-definitions.ts b/libs/common/src/services/event/key-definitions.ts index 1059d24b72..5682099688 100644 --- a/libs/common/src/services/event/key-definitions.ts +++ b/libs/common/src/services/event/key-definitions.ts @@ -1,10 +1,11 @@ import { EventData } from "../../models/data/event.data"; -import { KeyDefinition, EVENT_COLLECTION_DISK } from "../../platform/state"; +import { EVENT_COLLECTION_DISK, UserKeyDefinition } from "../../platform/state"; -export const EVENT_COLLECTION: KeyDefinition = KeyDefinition.array( +export const EVENT_COLLECTION = UserKeyDefinition.array( EVENT_COLLECTION_DISK, "events", { deserializer: (s) => EventData.fromJSON(s), + clearOn: ["logout"], }, ); diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index 4dc8772d00..7dc54b849f 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -2,6 +2,7 @@ import * as signalR from "@microsoft/signalr"; import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack"; import { firstValueFrom } from "rxjs"; +import { AuthRequestServiceAbstraction } from "../../../auth/src/common/abstractions"; import { ApiService } from "../abstractions/api.service"; import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service"; import { AuthService } from "../auth/abstractions/auth.service"; @@ -18,6 +19,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { LogService } from "../platform/abstractions/log.service"; import { MessagingService } from "../platform/abstractions/messaging.service"; import { StateService } from "../platform/abstractions/state.service"; +import { UserId } from "../types/guid"; import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction"; export class NotificationsService implements NotificationsServiceAbstraction { @@ -37,6 +39,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { private logoutCallback: (expired: boolean) => Promise, private stateService: StateService, private authService: AuthService, + private authRequestService: AuthRequestServiceAbstraction, private messagingService: MessagingService, ) { this.environmentService.environment$.subscribe(() => { @@ -199,10 +202,13 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification); break; case NotificationType.AuthRequest: - if (await this.stateService.getApproveLoginRequests()) { - this.messagingService.send("openLoginApproval", { - notificationId: notification.payload.id, - }); + { + const userId = await this.stateService.getUserId(); + if (await this.authRequestService.getAcceptAuthRequests(userId as UserId)) { + this.messagingService.send("openLoginApproval", { + notificationId: notification.payload.id, + }); + } } break; default: diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/services/search.service.ts index 773d51297a..38ddfe0e47 100644 --- a/libs/common/src/services/search.service.ts +++ b/libs/common/src/services/search.service.ts @@ -1,20 +1,91 @@ import * as lunr from "lunr"; +import { Observable, firstValueFrom, map } from "rxjs"; +import { Jsonify } from "type-fest"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { UriMatchStrategy } from "../models/domain/domain-service"; import { I18nService } from "../platform/abstractions/i18n.service"; import { LogService } from "../platform/abstractions/log.service"; +import { + ActiveUserState, + StateProvider, + UserKeyDefinition, + VAULT_SEARCH_MEMORY, +} from "../platform/state"; import { SendView } from "../tools/send/models/view/send.view"; +import { IndexedEntityId } from "../types/guid"; import { FieldType } from "../vault/enums"; import { CipherType } from "../vault/enums/cipher-type"; import { CipherView } from "../vault/models/view/cipher.view"; +export type SerializedLunrIndex = { + version: string; + fields: string[]; + fieldVectors: [string, number[]]; + invertedIndex: any[]; + pipeline: string[]; +}; + +/** + * The `KeyDefinition` for accessing the search index in application state. + * The key definition is configured to clear the index when the user locks the vault. + */ +export const LUNR_SEARCH_INDEX = new UserKeyDefinition( + VAULT_SEARCH_MEMORY, + "searchIndex", + { + deserializer: (obj: Jsonify) => obj, + clearOn: ["lock", "logout"], + }, +); + +/** + * The `KeyDefinition` for accessing the ID of the entity currently indexed by Lunr search. + * The key definition is configured to clear the indexed entity ID when the user locks the vault. + */ +export const LUNR_SEARCH_INDEXED_ENTITY_ID = new UserKeyDefinition( + VAULT_SEARCH_MEMORY, + "searchIndexedEntityId", + { + deserializer: (obj: Jsonify) => obj, + clearOn: ["lock", "logout"], + }, +); + +/** + * The `KeyDefinition` for accessing the state of Lunr search indexing, indicating whether the Lunr search index is currently being built or updating. + * The key definition is configured to clear the indexing state when the user locks the vault. + */ +export const LUNR_SEARCH_INDEXING = new UserKeyDefinition( + VAULT_SEARCH_MEMORY, + "isIndexing", + { + deserializer: (obj: Jsonify) => obj, + clearOn: ["lock", "logout"], + }, +); + export class SearchService implements SearchServiceAbstraction { private static registeredPipeline = false; - indexedEntityId?: string = null; - private indexing = false; - private index: lunr.Index = null; + private searchIndexState: ActiveUserState = + this.stateProvider.getActive(LUNR_SEARCH_INDEX); + private readonly index$: Observable = this.searchIndexState.state$.pipe( + map((searchIndex) => (searchIndex ? lunr.Index.load(searchIndex) : null)), + ); + + private searchIndexEntityIdState: ActiveUserState = this.stateProvider.getActive( + LUNR_SEARCH_INDEXED_ENTITY_ID, + ); + readonly indexedEntityId$: Observable = + this.searchIndexEntityIdState.state$.pipe(map((id) => id)); + + private searchIsIndexingState: ActiveUserState = + this.stateProvider.getActive(LUNR_SEARCH_INDEXING); + private readonly searchIsIndexing$: Observable = this.searchIsIndexingState.state$.pipe( + map((indexing) => indexing ?? false), + ); + private readonly immediateSearchLocales: string[] = ["zh-CN", "zh-TW", "ja", "ko", "vi"]; private readonly defaultSearchableMinLength: number = 2; private searchableMinLength: number = this.defaultSearchableMinLength; @@ -22,6 +93,7 @@ export class SearchService implements SearchServiceAbstraction { constructor( private logService: LogService, private i18nService: I18nService, + private stateProvider: StateProvider, ) { this.i18nService.locale$.subscribe((locale) => { if (this.immediateSearchLocales.indexOf(locale) !== -1) { @@ -40,28 +112,29 @@ export class SearchService implements SearchServiceAbstraction { } } - clearIndex(): void { - this.indexedEntityId = null; - this.index = null; + async clearIndex(): Promise { + await this.searchIndexEntityIdState.update(() => null); + await this.searchIndexState.update(() => null); + await this.searchIsIndexingState.update(() => null); } - isSearchable(query: string): boolean { + async isSearchable(query: string): Promise { query = SearchService.normalizeSearchQuery(query); + const index = await this.getIndexForSearch(); const notSearchable = query == null || - (this.index == null && query.length < this.searchableMinLength) || - (this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0); + (index == null && query.length < this.searchableMinLength) || + (index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0); return !notSearchable; } - indexCiphers(ciphers: CipherView[], indexedEntityId?: string): void { - if (this.indexing) { + async indexCiphers(ciphers: CipherView[], indexedEntityId?: string): Promise { + if (await this.getIsIndexing()) { return; } - this.indexing = true; - this.indexedEntityId = indexedEntityId; - this.index = null; + await this.setIsIndexing(true); + await this.setIndexedEntityIdForSearch(indexedEntityId as IndexedEntityId); const builder = new lunr.Builder(); builder.pipeline.add(this.normalizeAccentsPipelineFunction); builder.ref("id"); @@ -95,9 +168,11 @@ export class SearchService implements SearchServiceAbstraction { builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId }); ciphers = ciphers || []; ciphers.forEach((c) => builder.add(c)); - this.index = builder.build(); + const index = builder.build(); - this.indexing = false; + await this.setIndexForSearch(index.toJSON() as SerializedLunrIndex); + + await this.setIsIndexing(false); this.logService.info("Finished search indexing"); } @@ -125,18 +200,18 @@ export class SearchService implements SearchServiceAbstraction { ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean); } - if (!this.isSearchable(query)) { + if (!(await this.isSearchable(query))) { return ciphers; } - if (this.indexing) { + if (await this.getIsIndexing()) { await new Promise((r) => setTimeout(r, 250)); - if (this.indexing) { + if (await this.getIsIndexing()) { await new Promise((r) => setTimeout(r, 500)); } } - const index = this.getIndexForSearch(); + const index = await this.getIndexForSearch(); if (index == null) { // Fall back to basic search if index is not available return this.searchCiphersBasic(ciphers, query); @@ -230,8 +305,24 @@ export class SearchService implements SearchServiceAbstraction { return sendsMatched.concat(lowPriorityMatched); } - getIndexForSearch(): lunr.Index { - return this.index; + async getIndexForSearch(): Promise { + return await firstValueFrom(this.index$); + } + + private async setIndexForSearch(index: SerializedLunrIndex): Promise { + await this.searchIndexState.update(() => index); + } + + private async setIndexedEntityIdForSearch(indexedEntityId: IndexedEntityId): Promise { + await this.searchIndexEntityIdState.update(() => indexedEntityId); + } + + private async setIsIndexing(indexing: boolean): Promise { + await this.searchIsIndexingState.update(() => indexing); + } + + private async getIsIndexing(): Promise { + return await firstValueFrom(this.searchIsIndexing$); } private fieldExtractor(c: CipherView, joined: boolean) { diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index 4eb9e77699..a8afc63297 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -52,7 +52,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA await this.stateService.setVaultTimeoutAction(action); - await this.tokenService.setTokens(accessToken, refreshToken, action, timeout, [ + await this.tokenService.setTokens(accessToken, action, timeout, refreshToken, [ clientId, clientSecret, ]); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index e48f2fe0a3..243b644dd8 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -1,17 +1,21 @@ import { MockProxy, any, mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; import { Account } from "../../platform/models/domain/account"; import { StateEventRunnerService } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { CollectionService } from "../../vault/abstractions/collection.service"; import { FolderService } from "../../vault/abstractions/folder/folder.service.abstraction"; @@ -19,6 +23,8 @@ import { FolderService } from "../../vault/abstractions/folder/folder.service.ab import { VaultTimeoutService } from "./vault-timeout.service"; describe("VaultTimeoutService", () => { + let accountService: FakeAccountService; + let masterPasswordService: FakeMasterPasswordService; let cipherService: MockProxy; let folderService: MockProxy; let collectionService: MockProxy; @@ -39,7 +45,11 @@ describe("VaultTimeoutService", () => { let vaultTimeoutService: VaultTimeoutService; + const userId = Utils.newGuid() as UserId; + beforeEach(() => { + accountService = mockAccountServiceWith(userId); + masterPasswordService = new FakeMasterPasswordService(); cipherService = mock(); folderService = mock(); collectionService = mock(); @@ -66,6 +76,8 @@ describe("VaultTimeoutService", () => { availableVaultTimeoutActionsSubject = new BehaviorSubject([]); vaultTimeoutService = new VaultTimeoutService( + accountService, + masterPasswordService, cipherService, folderService, collectionService, @@ -123,6 +135,14 @@ describe("VaultTimeoutService", () => { stateService.activeAccount$ = new BehaviorSubject(globalSetups?.userId); + if (globalSetups?.userId) { + accountService.activeAccountSubject.next({ + id: globalSetups.userId as UserId, + email: null, + name: null, + }); + } + platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); vaultTimeoutSettingsService.vaultTimeoutAction$.mockImplementation((userId) => { @@ -156,8 +176,7 @@ describe("VaultTimeoutService", () => { expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); - expect(cryptoService.clearUserKey).toHaveBeenCalledWith(false, userId); - expect(cryptoService.clearMasterKey).toHaveBeenCalledWith(userId); + expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId); expect(cipherService.clearCache).toHaveBeenCalledWith(userId); expect(lockedCallback).toHaveBeenCalledWith(userId); }; diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index c3270ac2b8..35faf0fcee 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -3,7 +3,9 @@ import { firstValueFrom, timeout } from "rxjs"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { ClientType } from "../../enums"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; @@ -21,6 +23,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private inited = false; constructor( + private accountService: AccountService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, @@ -84,23 +88,20 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.logOut(userId); } - const currentUserId = await this.stateService.getUserId(); + const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; if (userId == null || userId === currentUserId) { - this.searchService.clearIndex(); + await this.searchService.clearIndex(); await this.folderService.clearCache(); await this.collectionService.clearActiveUserCache(); } + await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId); + await this.stateService.setEverBeenUnlocked(true, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); - await this.cryptoService.clearUserKey(false, userId); - await this.cryptoService.clearMasterKey(userId); - await this.cryptoService.clearOrgKeys(true, userId); - await this.cryptoService.clearKeyPair(true, userId); - await this.cipherService.clearCache(userId); await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 3c03854780..f9a8734731 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -43,7 +43,18 @@ import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decrypt import { MergeEnvironmentState } from "./migrations/45-merge-environment-state"; import { DeleteBiometricPromptCancelledData } from "./migrations/46-delete-orphaned-biometric-prompt-data"; import { MoveDesktopSettingsMigrator } from "./migrations/47-move-desktop-settings"; +import { MoveDdgToStateProviderMigrator } from "./migrations/48-move-ddg-to-state-provider"; +import { AccountServerConfigMigrator } from "./migrations/49-move-account-server-configs"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; +import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider"; +import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers"; +import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version"; +import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers"; +import { SendMigrator } from "./migrations/54-move-encrypted-sends"; +import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider"; +import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; +import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider"; +import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; @@ -51,7 +62,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 47; +export const CURRENT_VERSION = 58; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -100,7 +111,18 @@ export function createMigrationBuilder() { .with(UserDecryptionOptionsMigrator, 43, 44) .with(MergeEnvironmentState, 44, 45) .with(DeleteBiometricPromptCancelledData, 45, 46) - .with(MoveDesktopSettingsMigrator, 46, CURRENT_VERSION); + .with(MoveDesktopSettingsMigrator, 46, 47) + .with(MoveDdgToStateProviderMigrator, 47, 48) + .with(AccountServerConfigMigrator, 48, 49) + .with(KeyConnectorMigrator, 49, 50) + .with(RememberedEmailMigrator, 50, 51) + .with(DeleteInstalledVersion, 51, 52) + .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) + .with(SendMigrator, 53, 54) + .with(MoveMasterKeyStateToProviderMigrator, 54, 55) + .with(AuthRequestMigrator, 55, 56) + .with(CipherServiceMigrator, 56, 57) + .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migration-builder.spec.ts b/libs/common/src/state-migrations/migration-builder.spec.ts index f10d3b11a9..6a4ff8e6d4 100644 --- a/libs/common/src/state-migrations/migration-builder.spec.ts +++ b/libs/common/src/state-migrations/migration-builder.spec.ts @@ -83,35 +83,35 @@ describe("MigrationBuilder", () => { }); it("should migrate", async () => { - const helper = new MigrationHelper(0, mock(), mock()); + const helper = new MigrationHelper(0, mock(), mock(), "general"); const spy = jest.spyOn(migrator, "migrate"); await sut.migrate(helper); expect(spy).toBeCalledWith(helper); }); it("should rollback", async () => { - const helper = new MigrationHelper(1, mock(), mock()); + const helper = new MigrationHelper(1, mock(), mock(), "general"); const spy = jest.spyOn(rollback_migrator, "rollback"); await sut.migrate(helper); expect(spy).toBeCalledWith(helper); }); it("should update version on migrate", async () => { - const helper = new MigrationHelper(0, mock(), mock()); + const helper = new MigrationHelper(0, mock(), mock(), "general"); const spy = jest.spyOn(migrator, "updateVersion"); await sut.migrate(helper); expect(spy).toBeCalledWith(helper, "up"); }); it("should update version on rollback", async () => { - const helper = new MigrationHelper(1, mock(), mock()); + const helper = new MigrationHelper(1, mock(), mock(), "general"); const spy = jest.spyOn(rollback_migrator, "updateVersion"); await sut.migrate(helper); expect(spy).toBeCalledWith(helper, "down"); }); it("should not run the migrator if the current version does not match the from version", async () => { - const helper = new MigrationHelper(3, mock(), mock()); + const helper = new MigrationHelper(3, mock(), mock(), "general"); const migrate = jest.spyOn(migrator, "migrate"); const rollback = jest.spyOn(rollback_migrator, "rollback"); await sut.migrate(helper); @@ -120,7 +120,7 @@ describe("MigrationBuilder", () => { }); it("should not update version if the current version does not match the from version", async () => { - const helper = new MigrationHelper(3, mock(), mock()); + const helper = new MigrationHelper(3, mock(), mock(), "general"); const migrate = jest.spyOn(migrator, "updateVersion"); const rollback = jest.spyOn(rollback_migrator, "updateVersion"); await sut.migrate(helper); @@ -130,7 +130,7 @@ describe("MigrationBuilder", () => { }); it("should be able to call instance methods", async () => { - const helper = new MigrationHelper(0, mock(), mock()); + const helper = new MigrationHelper(0, mock(), mock(), "general"); await sut.with(TestMigratorWithInstanceMethod, 0, 1).migrate(helper); }); }); diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index e929877b63..f86cac8768 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -9,7 +9,7 @@ import { AbstractStorageService } from "../platform/abstractions/storage.service // eslint-disable-next-line import/no-restricted-paths -- Needed to generate unique strings for injection import { Utils } from "../platform/misc/utils"; -import { MigrationHelper } from "./migration-helper"; +import { MigrationHelper, MigrationHelperType } from "./migration-helper"; import { Migrator } from "./migrator"; const exampleJSON = { @@ -37,7 +37,7 @@ describe("RemoveLegacyEtmKeyMigrator", () => { storage = mock(); storage.get.mockImplementation((key) => (exampleJSON as any)[key]); - sut = new MigrationHelper(0, storage, logService); + sut = new MigrationHelper(0, storage, logService, "general"); }); describe("get", () => { @@ -150,6 +150,7 @@ describe("RemoveLegacyEtmKeyMigrator", () => { export function mockMigrationHelper( storageJson: any, stateVersion = 0, + type: MigrationHelperType = "general", ): MockProxy { const logService: MockProxy = mock(); const storage: MockProxy = mock(); @@ -157,7 +158,7 @@ export function mockMigrationHelper( storage.save.mockImplementation(async (key, value) => { (storageJson as any)[key] = value; }); - const helper = new MigrationHelper(stateVersion, storage, logService); + const helper = new MigrationHelper(stateVersion, storage, logService, type); const mockHelper = mock(); mockHelper.get.mockImplementation((key) => helper.get(key)); @@ -175,6 +176,9 @@ export function mockMigrationHelper( helper.setToUser(userId, keyDefinition, value), ); mockHelper.getAccounts.mockImplementation(() => helper.getAccounts()); + + mockHelper.type = helper.type; + return mockHelper; } @@ -291,7 +295,7 @@ export async function runMigrator< const allInjectedData = injectData(initalData, []); const fakeStorageService = new FakeStorageService(initalData); - const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock()); + const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock(), "general"); // Run their migrations if (direction === "rollback") { diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 315a343e9e..5b8e4ff93e 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -9,12 +9,29 @@ export type KeyDefinitionLike = { key: string; }; +export type MigrationHelperType = "general" | "web-disk-local"; + export class MigrationHelper { constructor( public currentVersion: number, private storageService: AbstractStorageService, public logService: LogService, - ) {} + type: MigrationHelperType, + ) { + this.type = type; + } + + /** + * On some clients, migrations are ran multiple times without direct action from the migration writer. + * + * All clients will run through migrations at least once, this run is referred to as `"general"`. If a migration is + * ran more than that single time, they will get a unique name if that the write can make conditional logic based on which + * migration run this is. + * + * @remarks The preferrable way of writing migrations is ALWAYS to be defensive and reflect on the data you are given back. This + * should really only be used when reflecting on the data given isn't enough. + */ + type: MigrationHelperType; /** * Gets a value from the storage service at the given key. diff --git a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts index a5243c261a..7dae6eeeb6 100644 --- a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts +++ b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.spec.ts @@ -124,65 +124,107 @@ describe("TokenServiceStateProviderMigrator", () => { sut = new TokenServiceStateProviderMigrator(37, 38); }); - it("should remove state service data from all accounts that have it", async () => { - await sut.migrate(helper); + describe("Session storage", () => { + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); - expect(helper.set).toHaveBeenCalledWith("user1", { - tokens: { - otherStuff: "overStuff2", - }, - profile: { - email: "user1Email", - otherStuff: "overStuff3", - }, - keys: { - otherStuff: "overStuff4", - }, - otherStuff: "otherStuff5", + expect(helper.set).toHaveBeenCalledWith("user1", { + tokens: { + otherStuff: "overStuff2", + }, + profile: { + email: "user1Email", + otherStuff: "overStuff3", + }, + keys: { + otherStuff: "overStuff4", + }, + otherStuff: "otherStuff5", + }); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + expect(helper.set).not.toHaveBeenCalledWith("user3", any()); }); - expect(helper.set).toHaveBeenCalledTimes(2); - expect(helper.set).not.toHaveBeenCalledWith("user2", any()); - expect(helper.set).not.toHaveBeenCalledWith("user3", any()); + it("should migrate data to state providers for defined accounts that have the data", async () => { + await sut.migrate(helper); + + // Two factor Token Migration + expect(helper.setToGlobal).toHaveBeenLastCalledWith( + EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, + { + user1Email: "twoFactorToken", + user2Email: "twoFactorToken", + }, + ); + expect(helper.setToGlobal).toHaveBeenCalledTimes(1); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", ACCESS_TOKEN_DISK, "accessToken"); + expect(helper.setToUser).toHaveBeenCalledWith("user1", REFRESH_TOKEN_DISK, "refreshToken"); + expect(helper.setToUser).toHaveBeenCalledWith( + "user1", + API_KEY_CLIENT_ID_DISK, + "apiKeyClientId", + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "user1", + API_KEY_CLIENT_SECRET_DISK, + "apiKeyClientSecret", + ); + + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", ACCESS_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", REFRESH_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", API_KEY_CLIENT_ID_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith( + "user2", + API_KEY_CLIENT_SECRET_DISK, + any(), + ); + + // Expect that we didn't migrate anything to user 3 + + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", ACCESS_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", REFRESH_TOKEN_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", API_KEY_CLIENT_ID_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith( + "user3", + API_KEY_CLIENT_SECRET_DISK, + any(), + ); + }); }); + describe("Local storage", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 37, "web-disk-local"); + }); + it("should remove state service data from all accounts that have it", async () => { + await sut.migrate(helper); - it("should migrate data to state providers for defined accounts that have the data", async () => { - await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("user1", { + tokens: { + otherStuff: "overStuff2", + }, + profile: { + email: "user1Email", + otherStuff: "overStuff3", + }, + keys: { + otherStuff: "overStuff4", + }, + otherStuff: "otherStuff5", + }); - // Two factor Token Migration - expect(helper.setToGlobal).toHaveBeenLastCalledWith( - EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, - { - user1Email: "twoFactorToken", - user2Email: "twoFactorToken", - }, - ); - expect(helper.setToGlobal).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + expect(helper.set).not.toHaveBeenCalledWith("user3", any()); + }); - expect(helper.setToUser).toHaveBeenCalledWith("user1", ACCESS_TOKEN_DISK, "accessToken"); - expect(helper.setToUser).toHaveBeenCalledWith("user1", REFRESH_TOKEN_DISK, "refreshToken"); - expect(helper.setToUser).toHaveBeenCalledWith( - "user1", - API_KEY_CLIENT_ID_DISK, - "apiKeyClientId", - ); - expect(helper.setToUser).toHaveBeenCalledWith( - "user1", - API_KEY_CLIENT_SECRET_DISK, - "apiKeyClientSecret", - ); + it("should not migrate any data to local storage", async () => { + await sut.migrate(helper); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", ACCESS_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", REFRESH_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", API_KEY_CLIENT_ID_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user2", API_KEY_CLIENT_SECRET_DISK, any()); - - // Expect that we didn't migrate anything to user 3 - - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", ACCESS_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", REFRESH_TOKEN_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", API_KEY_CLIENT_ID_DISK, any()); - expect(helper.setToUser).not.toHaveBeenCalledWith("user3", API_KEY_CLIENT_SECRET_DISK, any()); + expect(helper.setToUser).not.toHaveBeenCalled(); + }); }); }); diff --git a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts index 17753d2187..640e63cdc5 100644 --- a/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts @@ -84,7 +84,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { if (existingAccessToken != null) { // Only migrate data that exists - await helper.setToUser(userId, ACCESS_TOKEN_DISK, existingAccessToken); + if (helper.type !== "web-disk-local") { + // only migrate access token to session storage - never local. + await helper.setToUser(userId, ACCESS_TOKEN_DISK, existingAccessToken); + } delete account.tokens.accessToken; updatedAccount = true; } @@ -93,7 +96,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { const existingRefreshToken = account?.tokens?.refreshToken; if (existingRefreshToken != null) { - await helper.setToUser(userId, REFRESH_TOKEN_DISK, existingRefreshToken); + if (helper.type !== "web-disk-local") { + // only migrate refresh token to session storage - never local. + await helper.setToUser(userId, REFRESH_TOKEN_DISK, existingRefreshToken); + } delete account.tokens.refreshToken; updatedAccount = true; } @@ -102,7 +108,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { const existingApiKeyClientId = account?.profile?.apiKeyClientId; if (existingApiKeyClientId != null) { - await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, existingApiKeyClientId); + if (helper.type !== "web-disk-local") { + // only migrate client id to session storage - never local. + await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, existingApiKeyClientId); + } delete account.profile.apiKeyClientId; updatedAccount = true; } @@ -110,7 +119,10 @@ export class TokenServiceStateProviderMigrator extends Migrator<37, 38> { // Migrate API key client secret const existingApiKeyClientSecret = account?.keys?.apiKeyClientSecret; if (existingApiKeyClientSecret != null) { - await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, existingApiKeyClientSecret); + if (helper.type !== "web-disk-local") { + // only migrate client secret to session storage - never local. + await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, existingApiKeyClientSecret); + } delete account.keys.apiKeyClientSecret; updatedAccount = true; } diff --git a/libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.spec.ts new file mode 100644 index 0000000000..6b6ebffb58 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.spec.ts @@ -0,0 +1,47 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { MoveDdgToStateProviderMigrator } from "./48-move-ddg-to-state-provider"; + +describe("MoveDdgToStateProviderMigrator", () => { + const migrator = new MoveDdgToStateProviderMigrator(47, 48); + + it("migrate", async () => { + const output = await runMigrator(migrator, { + global: { + enableDuckDuckGoBrowserIntegration: true, + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }); + + expect(output).toEqual({ + global_autofillSettings_enableDuckDuckGoBrowserIntegration: true, + global: { + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }); + }); + + it("rollback", async () => { + const output = await runMigrator( + migrator, + { + global_autofillSettings_enableDuckDuckGoBrowserIntegration: true, + global: { + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }, + "rollback", + ); + + expect(output).toEqual({ + global: { + enableDuckDuckGoBrowserIntegration: true, + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.ts b/libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.ts new file mode 100644 index 0000000000..51676c1d7b --- /dev/null +++ b/libs/common/src/state-migrations/migrations/48-move-ddg-to-state-provider.ts @@ -0,0 +1,40 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedGlobal = { + enableDuckDuckGoBrowserIntegration?: boolean; +}; + +export const DDG_KEY: KeyDefinitionLike = { + key: "enableDuckDuckGoBrowserIntegration", + stateDefinition: { + name: "autofillSettings", + }, +}; + +export class MoveDdgToStateProviderMigrator extends Migrator<47, 48> { + async migrate(helper: MigrationHelper): Promise { + // global state + const global = await helper.get("global"); + if (global?.enableDuckDuckGoBrowserIntegration == null) { + return; + } + + await helper.setToGlobal(DDG_KEY, global.enableDuckDuckGoBrowserIntegration); + delete global.enableDuckDuckGoBrowserIntegration; + await helper.set("global", global); + } + + async rollback(helper: MigrationHelper): Promise { + const enableDdg = await helper.getFromGlobal(DDG_KEY); + + if (!enableDdg) { + return; + } + + const global = (await helper.get("global")) ?? {}; + global.enableDuckDuckGoBrowserIntegration = enableDdg; + await helper.set("global", global); + await helper.removeFromGlobal(DDG_KEY); + } +} diff --git a/libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts new file mode 100644 index 0000000000..4533a754b6 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.spec.ts @@ -0,0 +1,112 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { AccountServerConfigMigrator } from "./49-move-account-server-configs"; + +describe("AccountServerConfigMigrator", () => { + const migrator = new AccountServerConfigMigrator(48, 49); + + describe("all data", () => { + function toMigrate() { + return { + authenticatedAccounts: ["user1", "user2"], + user1: { + settings: { + serverConfig: { + config: "user1 server config", + }, + }, + }, + user2: { + settings: { + serverConfig: { + config: "user2 server config", + }, + }, + }, + }; + } + + function migrated() { + return { + authenticatedAccounts: ["user1", "user2"], + + user1: { + settings: {}, + }, + user2: { + settings: {}, + }, + user_user1_config_serverConfig: { + config: "user1 server config", + }, + user_user2_config_serverConfig: { + config: "user2 server config", + }, + }; + } + + function rolledBack(previous: object) { + return { + ...previous, + user_user1_config_serverConfig: null as unknown, + user_user2_config_serverConfig: null as unknown, + }; + } + + it("migrates", async () => { + const output = await runMigrator(migrator, toMigrate(), "migrate"); + expect(output).toEqual(migrated()); + }); + + it("rolls back", async () => { + const output = await runMigrator(migrator, migrated(), "rollback"); + expect(output).toEqual(rolledBack(toMigrate())); + }); + }); + + describe("missing parts", () => { + function toMigrate() { + return { + authenticatedAccounts: ["user1", "user2"], + user1: { + settings: { + serverConfig: { + config: "user1 server config", + }, + }, + }, + user2: null as unknown, + }; + } + + function migrated() { + return { + authenticatedAccounts: ["user1", "user2"], + user1: { + settings: {}, + }, + user2: null as unknown, + user_user1_config_serverConfig: { + config: "user1 server config", + }, + }; + } + + function rollback(previous: object) { + return { + ...previous, + user_user1_config_serverConfig: null as unknown, + }; + } + + it("migrates", async () => { + const output = await runMigrator(migrator, toMigrate(), "migrate"); + expect(output).toEqual(migrated()); + }); + + it("rolls back", async () => { + const output = await runMigrator(migrator, migrated(), "rollback"); + expect(output).toEqual(rollback(toMigrate())); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts new file mode 100644 index 0000000000..8cc25a322d --- /dev/null +++ b/libs/common/src/state-migrations/migrations/49-move-account-server-configs.ts @@ -0,0 +1,51 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +const CONFIG_DISK: StateDefinitionLike = { name: "config" }; +export const USER_SERVER_CONFIG: KeyDefinitionLike = { + stateDefinition: CONFIG_DISK, + key: "serverConfig", +}; + +// Note: no need to migrate global configs, they don't currently exist + +type ExpectedAccountType = { + settings?: { + serverConfig?: unknown; + }; +}; + +export class AccountServerConfigMigrator extends Migrator<48, 49> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + if (account?.settings?.serverConfig != null) { + await helper.setToUser(userId, USER_SERVER_CONFIG, account.settings.serverConfig); + delete account.settings.serverConfig; + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const serverConfig = await helper.getFromUser(userId, USER_SERVER_CONFIG); + + if (serverConfig) { + account ??= {}; + account.settings ??= {}; + + account.settings.serverConfig = serverConfig; + await helper.setToUser(userId, USER_SERVER_CONFIG, null); + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts new file mode 100644 index 0000000000..2b96080821 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.spec.ts @@ -0,0 +1,174 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { KeyConnectorMigrator } from "./50-move-key-connector-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + FirstAccount: { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: false, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: true, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_keyConnector_usesKeyConnector: true, + user_FirstAccount_keyConnector_convertAccountToKeyConnector: false, + user_SecondAccount_keyConnector_usesKeyConnector: true, + user_SecondAccount_keyConnector_convertAccountToKeyConnector: true, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + FirstAccount: { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const usesKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "usesKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "convertAccountToKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +describe("KeyConnectorMigrator", () => { + let helper: MockProxy; + let sut: KeyConnectorMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 50); + sut = new KeyConnectorMigrator(49, 50); + }); + + it("should remove usesKeyConnector and convertAccountToKeyConnector from Profile", async () => { + await sut.migrate(helper); + + // Set is called 2 times even though there are 3 accounts. Since the target properties don't exist in ThirdAccount, they are not set. + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + usesKeyConnectorKeyDefinition, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + convertAccountToKeyConnectorKeyDefinition, + false, + ); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + usesKeyConnectorKeyDefinition, + true, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + convertAccountToKeyConnectorKeyDefinition, + true, + ); + expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount"); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 50); + sut = new KeyConnectorMigrator(49, 50); + }); + + it("should null out new usesKeyConnector global value", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(4); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + usesKeyConnectorKeyDefinition, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + convertAccountToKeyConnectorKeyDefinition, + null, + ); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: false, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + usesKeyConnectorKeyDefinition, + null, + ); + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + convertAccountToKeyConnectorKeyDefinition, + null, + ); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + usesKeyConnector: true, + convertAccountToKeyConnector: true, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + expect(helper.setToUser).not.toHaveBeenCalledWith("ThirdAccount"); + expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount"); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts new file mode 100644 index 0000000000..0deb7d5e2c --- /dev/null +++ b/libs/common/src/state-migrations/migrations/50-move-key-connector-to-state-provider.ts @@ -0,0 +1,78 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + profile?: { + usesKeyConnector?: boolean; + convertAccountToKeyConnector?: boolean; + }; +}; + +const usesKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "usesKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +const convertAccountToKeyConnectorKeyDefinition: KeyDefinitionLike = { + key: "convertAccountToKeyConnector", + stateDefinition: { + name: "keyConnector", + }, +}; + +export class KeyConnectorMigrator extends Migrator<49, 50> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const usesKeyConnector = account?.profile?.usesKeyConnector; + const convertAccountToKeyConnector = account?.profile?.convertAccountToKeyConnector; + if (usesKeyConnector == null && convertAccountToKeyConnector == null) { + return; + } + if (usesKeyConnector != null) { + await helper.setToUser(userId, usesKeyConnectorKeyDefinition, usesKeyConnector); + delete account.profile.usesKeyConnector; + } + if (convertAccountToKeyConnector != null) { + await helper.setToUser( + userId, + convertAccountToKeyConnectorKeyDefinition, + convertAccountToKeyConnector, + ); + delete account.profile.convertAccountToKeyConnector; + } + await helper.set(userId, account); + } + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const usesKeyConnector: boolean = await helper.getFromUser( + userId, + usesKeyConnectorKeyDefinition, + ); + const convertAccountToKeyConnector: boolean = await helper.getFromUser( + userId, + convertAccountToKeyConnectorKeyDefinition, + ); + if (usesKeyConnector == null && convertAccountToKeyConnector == null) { + return; + } + if (usesKeyConnector != null) { + account.profile.usesKeyConnector = usesKeyConnector; + await helper.setToUser(userId, usesKeyConnectorKeyDefinition, null); + } + if (convertAccountToKeyConnector != null) { + account.profile.convertAccountToKeyConnector = convertAccountToKeyConnector; + await helper.setToUser(userId, convertAccountToKeyConnectorKeyDefinition, null); + } + await helper.set(userId, account); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts new file mode 100644 index 0000000000..f36b5842aa --- /dev/null +++ b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.spec.ts @@ -0,0 +1,81 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper, runMigrator } from "../migration-helper.spec"; + +import { RememberedEmailMigrator } from "./51-move-remembered-email-to-state-providers"; + +function rollbackJSON() { + return { + global: { + extra: "data", + }, + global_loginEmail_storedEmail: "user@example.com", + }; +} + +describe("RememberedEmailMigrator", () => { + const migrator = new RememberedEmailMigrator(50, 51); + + describe("migrate", () => { + it("should migrate the rememberedEmail property from the legacy global object to a global StorageKey as 'global_loginEmail_storedEmail'", async () => { + const output = await runMigrator(migrator, { + global: { + rememberedEmail: "user@example.com", + extra: "data", // Represents a global property that should persist after migration + }, + }); + + expect(output).toEqual({ + global: { + extra: "data", + }, + global_loginEmail_storedEmail: "user@example.com", + }); + }); + + it("should remove the rememberedEmail property from the legacy global object", async () => { + const output = await runMigrator(migrator, { + global: { + rememberedEmail: "user@example.com", + }, + }); + + expect(output.global).not.toHaveProperty("rememberedEmail"); + }); + }); + + describe("rollback", () => { + let helper: MockProxy; + let sut: RememberedEmailMigrator; + + const keyDefinitionLike = { + key: "storedEmail", + stateDefinition: { + name: "loginEmail", + }, + }; + + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 51); + sut = new RememberedEmailMigrator(50, 51); + }); + + it("should null out the storedEmail global StorageKey", async () => { + await sut.rollback(helper); + + expect(helper.setToGlobal).toHaveBeenCalledTimes(1); + expect(helper.setToGlobal).toHaveBeenCalledWith(keyDefinitionLike, null); + }); + + it("should add the rememberedEmail property back to legacy global object", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).toHaveBeenCalledWith("global", { + rememberedEmail: "user@example.com", + extra: "data", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts new file mode 100644 index 0000000000..b2b0818719 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/51-move-remembered-email-to-state-providers.ts @@ -0,0 +1,46 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedGlobalState = { rememberedEmail?: string }; + +const LOGIN_EMAIL_STATE: StateDefinitionLike = { name: "loginEmail" }; + +const STORED_EMAIL: KeyDefinitionLike = { + key: "storedEmail", + stateDefinition: LOGIN_EMAIL_STATE, +}; + +export class RememberedEmailMigrator extends Migrator<50, 51> { + async migrate(helper: MigrationHelper): Promise { + const legacyGlobal = await helper.get("global"); + + // Move global data + if (legacyGlobal?.rememberedEmail != null) { + await helper.setToGlobal(STORED_EMAIL, legacyGlobal.rememberedEmail); + } + + // Delete legacy global data + delete legacyGlobal?.rememberedEmail; + await helper.set("global", legacyGlobal); + } + + async rollback(helper: MigrationHelper): Promise { + let legacyGlobal = await helper.get("global"); + let updatedLegacyGlobal = false; + const globalStoredEmail = await helper.getFromGlobal(STORED_EMAIL); + + if (globalStoredEmail) { + if (!legacyGlobal) { + legacyGlobal = {}; + } + + updatedLegacyGlobal = true; + legacyGlobal.rememberedEmail = globalStoredEmail; + await helper.setToGlobal(STORED_EMAIL, null); + } + + if (updatedLegacyGlobal) { + await helper.set("global", legacyGlobal); + } + } +} diff --git a/libs/common/src/state-migrations/migrations/52-delete-installed-version.spec.ts b/libs/common/src/state-migrations/migrations/52-delete-installed-version.spec.ts new file mode 100644 index 0000000000..752f1297ff --- /dev/null +++ b/libs/common/src/state-migrations/migrations/52-delete-installed-version.spec.ts @@ -0,0 +1,35 @@ +import { runMigrator } from "../migration-helper.spec"; + +import { DeleteInstalledVersion } from "./52-delete-installed-version"; + +describe("DeleteInstalledVersion", () => { + const sut = new DeleteInstalledVersion(51, 52); + + describe("migrate", () => { + it("can delete data if there", async () => { + const output = await runMigrator(sut, { + authenticatedAccounts: ["user1"], + global: { + installedVersion: "2024.1.1", + }, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user1"], + global: {}, + }); + }); + + it("will run if installed version is not there", async () => { + const output = await runMigrator(sut, { + authenticatedAccounts: ["user1"], + global: {}, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user1"], + global: {}, + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/52-delete-installed-version.ts b/libs/common/src/state-migrations/migrations/52-delete-installed-version.ts new file mode 100644 index 0000000000..7eea0e587c --- /dev/null +++ b/libs/common/src/state-migrations/migrations/52-delete-installed-version.ts @@ -0,0 +1,19 @@ +import { MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedGlobal = { + installedVersion?: string; +}; + +export class DeleteInstalledVersion extends Migrator<51, 52> { + async migrate(helper: MigrationHelper): Promise { + const legacyGlobal = await helper.get("global"); + if (legacyGlobal?.installedVersion != null) { + delete legacyGlobal.installedVersion; + await helper.set("global", legacyGlobal); + } + } + rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts new file mode 100644 index 0000000000..79366a4716 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts @@ -0,0 +1,171 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + DEVICE_KEY, + DeviceTrustCryptoServiceStateProviderMigrator, + SHOULD_TRUST_DEVICE, +} from "./53-migrate-device-trust-crypto-svc-to-state-providers"; + +// Represents data in state service pre-migration +function preMigrationJson() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2", "user3"], + user1: { + keys: { + deviceKey: { + keyB64: "user1_deviceKey", + }, + otherStuff: "overStuff2", + }, + settings: { + trustDeviceChoiceForDecryption: true, + otherStuff: "overStuff3", + }, + otherStuff: "otherStuff4", + }, + user2: { + keys: { + // no device key + otherStuff: "otherStuff5", + }, + settings: { + // no trust device choice + otherStuff: "overStuff6", + }, + otherStuff: "otherStuff7", + }, + }; +} + +function rollbackJSON() { + return { + // use pattern user_{userId}_{stateDefinitionName}_{keyDefinitionKey} for each user + // User1 migrated data + user_user1_deviceTrust_deviceKey: { + keyB64: "user1_deviceKey", + }, + user_user1_deviceTrust_shouldTrustDevice: true, + + // User2 does not have migrated data + + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2", "user3"], + user1: { + keys: { + otherStuff: "overStuff2", + }, + settings: { + otherStuff: "overStuff3", + }, + otherStuff: "otherStuff4", + }, + user2: { + keys: { + otherStuff: "otherStuff5", + }, + settings: { + otherStuff: "overStuff6", + }, + otherStuff: "otherStuff6", + }, + }; +} + +describe("DeviceTrustCryptoServiceStateProviderMigrator", () => { + let helper: MockProxy; + let sut: DeviceTrustCryptoServiceStateProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 52); + sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + }); + + // it should remove deviceKey and trustDeviceChoiceForDecryption from all accounts + it("should remove deviceKey and trustDeviceChoiceForDecryption from all accounts that have it", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("user1", { + keys: { + otherStuff: "overStuff2", + }, + settings: { + otherStuff: "overStuff3", + }, + otherStuff: "otherStuff4", + }); + + expect(helper.set).toHaveBeenCalledTimes(1); + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + expect(helper.set).not.toHaveBeenCalledWith("user3", any()); + }); + + it("should migrate deviceKey and trustDeviceChoiceForDecryption to state providers for accounts that have the data", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", DEVICE_KEY, { + keyB64: "user1_deviceKey", + }); + expect(helper.setToUser).toHaveBeenCalledWith("user1", SHOULD_TRUST_DEVICE, true); + + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", DEVICE_KEY, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", SHOULD_TRUST_DEVICE, any()); + + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", DEVICE_KEY, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user3", SHOULD_TRUST_DEVICE, any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 53); + sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + }); + + it("should null out newly migrated entries in state provider framework", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", DEVICE_KEY, null); + expect(helper.setToUser).toHaveBeenCalledWith("user1", SHOULD_TRUST_DEVICE, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user2", DEVICE_KEY, null); + expect(helper.setToUser).toHaveBeenCalledWith("user2", SHOULD_TRUST_DEVICE, null); + + expect(helper.setToUser).toHaveBeenCalledWith("user3", DEVICE_KEY, null); + expect(helper.setToUser).toHaveBeenCalledWith("user3", SHOULD_TRUST_DEVICE, null); + }); + + it("should add back deviceKey and trustDeviceChoiceForDecryption to all accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + keys: { + deviceKey: { + keyB64: "user1_deviceKey", + }, + otherStuff: "overStuff2", + }, + settings: { + trustDeviceChoiceForDecryption: true, + otherStuff: "overStuff3", + }, + otherStuff: "otherStuff4", + }); + }); + + it("should not add data back if data wasn't migrated or acct doesn't exist", async () => { + await sut.rollback(helper); + + // no data to add back for user2 (acct exists but no migrated data) and user3 (no acct) + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + expect(helper.set).not.toHaveBeenCalledWith("user3", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts new file mode 100644 index 0000000000..e19c7b3fa5 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts @@ -0,0 +1,95 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +// Types to represent data as it is stored in JSON +type DeviceKeyJsonType = { + keyB64: string; +}; + +type ExpectedAccountType = { + keys?: { + deviceKey?: DeviceKeyJsonType; + }; + settings?: { + trustDeviceChoiceForDecryption?: boolean; + }; +}; + +export const DEVICE_KEY: KeyDefinitionLike = { + key: "deviceKey", // matches KeyDefinition.key in DeviceTrustCryptoService + stateDefinition: { + name: "deviceTrust", // matches StateDefinition.name in StateDefinitions + }, +}; + +export const SHOULD_TRUST_DEVICE: KeyDefinitionLike = { + key: "shouldTrustDevice", + stateDefinition: { + name: "deviceTrust", + }, +}; + +export class DeviceTrustCryptoServiceStateProviderMigrator extends Migrator<52, 53> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedAccount = false; + + // Migrate deviceKey + const existingDeviceKey = account?.keys?.deviceKey; + + if (existingDeviceKey != null) { + // Only migrate data that exists + await helper.setToUser(userId, DEVICE_KEY, existingDeviceKey); + delete account.keys.deviceKey; + updatedAccount = true; + } + + // Migrate shouldTrustDevice + const existingShouldTrustDevice = account?.settings?.trustDeviceChoiceForDecryption; + + if (existingShouldTrustDevice != null) { + await helper.setToUser(userId, SHOULD_TRUST_DEVICE, existingShouldTrustDevice); + delete account.settings.trustDeviceChoiceForDecryption; + updatedAccount = true; + } + + if (updatedAccount) { + // Save the migrated account + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + // Rollback deviceKey + const migratedDeviceKey: DeviceKeyJsonType = await helper.getFromUser(userId, DEVICE_KEY); + + if (account?.keys && migratedDeviceKey != null) { + account.keys.deviceKey = migratedDeviceKey; + await helper.set(userId, account); + } + + await helper.setToUser(userId, DEVICE_KEY, null); + + // Rollback shouldTrustDevice + const migratedShouldTrustDevice = await helper.getFromUser( + userId, + SHOULD_TRUST_DEVICE, + ); + + if (account?.settings && migratedShouldTrustDevice != null) { + account.settings.trustDeviceChoiceForDecryption = migratedShouldTrustDevice; + await helper.set(userId, account); + } + + await helper.setToUser(userId, SHOULD_TRUST_DEVICE, null); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.spec.ts b/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.spec.ts new file mode 100644 index 0000000000..9e73a1258a --- /dev/null +++ b/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.spec.ts @@ -0,0 +1,236 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { SendMigrator } from "./54-move-encrypted-sends"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2"], + "user-1": { + data: { + sends: { + encrypted: { + "2ebadc23-e101-471b-bf2d-b125015337a0": { + id: "2ebadc23-e101-471b-bf2d-b125015337a0", + accessId: "I9y6LgHhG0e_LbElAVM3oA", + deletionDate: "2024-03-07T20:35:03Z", + disabled: false, + hideEmail: false, + key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=", + name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=", + text: { + hidden: false, + text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=", + }, + type: 0, + }, + "3b31c20d-b783-4912-9170-b12501555398": { + id: "3b31c20d-b783-4912-9170-b12501555398", + accessId: "DcIxO4O3EkmRcLElAVVTmA", + deletionDate: "2024-03-07T20:42:43Z", + disabled: false, + hideEmail: false, + key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=", + name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=", + text: { + hidden: false, + text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=", + }, + type: 0, + }, + }, + }, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + data: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + "user_user-1_send_sends": { + "2ebadc23-e101-471b-bf2d-b125015337a0": { + id: "2ebadc23-e101-471b-bf2d-b125015337a0", + accessId: "I9y6LgHhG0e_LbElAVM3oA", + deletionDate: "2024-03-07T20:35:03Z", + disabled: false, + hideEmail: false, + key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=", + name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=", + text: { + hidden: false, + text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=", + }, + type: 0, + }, + "3b31c20d-b783-4912-9170-b12501555398": { + id: "3b31c20d-b783-4912-9170-b12501555398", + accessId: "DcIxO4O3EkmRcLElAVVTmA", + deletionDate: "2024-03-07T20:42:43Z", + disabled: false, + hideEmail: false, + key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=", + name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=", + text: { + hidden: false, + text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=", + }, + type: 0, + }, + }, + "user_user-2_send_data": null as any, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user-1", "user-2"], + "user-1": { + data: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + "user-2": { + data: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +describe("SendMigrator", () => { + let helper: MockProxy; + let sut: SendMigrator; + const keyDefinitionLike = { + stateDefinition: { + name: "send", + }, + key: "sends", + }; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 53); + sut = new SendMigrator(53, 54); + }); + + it("should remove encrypted sends from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("user-1", { + data: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + }); + + it("should set encrypted sends for each account", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, { + "2ebadc23-e101-471b-bf2d-b125015337a0": { + id: "2ebadc23-e101-471b-bf2d-b125015337a0", + accessId: "I9y6LgHhG0e_LbElAVM3oA", + deletionDate: "2024-03-07T20:35:03Z", + disabled: false, + hideEmail: false, + key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=", + name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=", + text: { + hidden: false, + text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=", + }, + type: 0, + }, + "3b31c20d-b783-4912-9170-b12501555398": { + id: "3b31c20d-b783-4912-9170-b12501555398", + accessId: "DcIxO4O3EkmRcLElAVVTmA", + deletionDate: "2024-03-07T20:42:43Z", + disabled: false, + hideEmail: false, + key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=", + name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=", + text: { + hidden: false, + text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=", + }, + type: 0, + }, + }); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 54); + sut = new SendMigrator(53, 54); + }); + + it.each(["user-1", "user-2"])("should null out new values", async (userId) => { + await sut.rollback(helper); + expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null); + }); + + it("should add encrypted send values back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalled(); + expect(helper.set).toHaveBeenCalledWith("user-1", { + data: { + sends: { + encrypted: { + "2ebadc23-e101-471b-bf2d-b125015337a0": { + id: "2ebadc23-e101-471b-bf2d-b125015337a0", + accessId: "I9y6LgHhG0e_LbElAVM3oA", + deletionDate: "2024-03-07T20:35:03Z", + disabled: false, + hideEmail: false, + key: "2.sR07sf4f18Rw6YQH9R/fPw==|DlLIYdTlFBktHVEJixqrOZmW/dTDGmZ+9iVftYkRh4s=|2mXH2fKgtItEMi8rcP1ykkVwRbxztw5MGboBwRl/kKM=", + name: "2.A0wIvDbyzuh6AjgFtv2gqQ==|D0FymzfCdYJQcAk5MARfjg==|2g52y7e/33A7Bafaaoy3Yvae7vxbIxoABZdZeoZuyg4=", + text: { + hidden: false, + text: "2.MkcPiJUnNfpcyETsoH3b8g==|/oHZ5g6pmcerXAJidP9sXg==|JDhd1Blsxm/ubp2AAggHZr6gZhyW4UYwZkF5rxlO6X0=", + }, + type: 0, + }, + "3b31c20d-b783-4912-9170-b12501555398": { + id: "3b31c20d-b783-4912-9170-b12501555398", + accessId: "DcIxO4O3EkmRcLElAVVTmA", + deletionDate: "2024-03-07T20:42:43Z", + disabled: false, + hideEmail: false, + key: "2.366XwLCi7RJnXuAvpsEVNw==|XfLoSsdOIYsHfcSMmv+7VJY97bKfS3fjpbq3ez+KCdk=|iTJxf4Pc3ub6hTFXGeU8NpUV3KxnuxzaHuNoFo/I6Vs=", + name: "2.uJ2FoouFJr/SR9gv3jYY/Q==|ksVre4/YqwY/XOtPyIfIJw==|/LVT842LJgyAchl7NffogXkrmCFwOEHX9NFd0zgLqKo=", + text: { + hidden: false, + text: "2.zBeOzMKtjnP5YI5lJWQTWA==|vxrGt4GKtydhrqaW35b/jw==|36Jtg172awn9YsgfzNs4pJ/OpA59NBnUkLNt6lg7Zw8=", + }, + type: 0, + }, + }, + }, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + }); + + it("should not try to restore values to missing accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("user-3", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts b/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts new file mode 100644 index 0000000000..7f60d18ffe --- /dev/null +++ b/libs/common/src/state-migrations/migrations/54-move-encrypted-sends.ts @@ -0,0 +1,67 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +export enum SendType { + Text = 0, + File = 1, +} + +type SendData = { + id: string; + accessId: string; +}; + +type ExpectedSendState = { + data?: { + sends?: { + encrypted?: Record; + }; + }; +}; + +const ENCRYPTED_SENDS: KeyDefinitionLike = { + stateDefinition: { + name: "send", + }, + key: "sends", +}; + +/** + * Only encrypted sends are stored on disk. Only the encrypted items need to be + * migrated from the previous sends state data. + */ +export class SendMigrator extends Migrator<53, 54> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function migrateAccount(userId: string, account: ExpectedSendState): Promise { + const value = account?.data?.sends?.encrypted; + if (value != null) { + await helper.setToUser(userId, ENCRYPTED_SENDS, value); + delete account.data.sends; + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function rollbackAccount(userId: string, account: ExpectedSendState): Promise { + const value = await helper.getFromUser(userId, ENCRYPTED_SENDS); + if (account) { + account.data = Object.assign(account.data ?? {}, { + sends: { + encrypted: value, + }, + }); + + await helper.set(userId, account); + } + await helper.setToUser(userId, ENCRYPTED_SENDS, null); + } + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts new file mode 100644 index 0000000000..bbf0352e95 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.spec.ts @@ -0,0 +1,210 @@ +import { any, MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + FORCE_SET_PASSWORD_REASON_DEFINITION, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + MASTER_KEY_HASH_DEFINITION, + MoveMasterKeyStateToProviderMigrator, +} from "./55-move-master-key-state-to-provider"; + +function preMigrationState() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + // prettier-ignore + "FirstAccount": { + profile: { + forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + keyHash: "FirstAccount_keyHash", + otherStuff: "overStuff2", + }, + keys: { + masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff3", + }, + // prettier-ignore + "SecondAccount": { + profile: { + forceSetPasswordReason: "SecondAccount_forceSetPasswordReason", + keyHash: "SecondAccount_keyHash", + otherStuff: "otherStuff4", + }, + keys: { + masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff5", + }, + // prettier-ignore + "ThirdAccount": { + profile: { + otherStuff: "otherStuff6", + }, + }, + }; +} + +function postMigrationState() { + return { + user_FirstAccount_masterPassword_forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + user_FirstAccount_masterPassword_masterKeyHash: "FirstAccount_keyHash", + user_FirstAccount_masterPassword_masterKeyEncryptedUserKey: + "FirstAccount_masterKeyEncryptedUserKey", + user_SecondAccount_masterPassword_forceSetPasswordReason: + "SecondAccount_forceSetPasswordReason", + user_SecondAccount_masterPassword_masterKeyHash: "SecondAccount_keyHash", + user_SecondAccount_masterPassword_masterKeyEncryptedUserKey: + "SecondAccount_masterKeyEncryptedUserKey", + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + // prettier-ignore + "FirstAccount": { + profile: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }, + // prettier-ignore + "SecondAccount": { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + // prettier-ignore + "ThirdAccount": { + profile: { + otherStuff: "otherStuff6", + }, + }, + }; +} + +describe("MoveForceSetPasswordReasonToStateProviderMigrator", () => { + let helper: MockProxy; + let sut: MoveMasterKeyStateToProviderMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationState(), 54); + sut = new MoveMasterKeyStateToProviderMigrator(54, 55); + }); + + it("should remove properties from existing accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "overStuff2", + }, + keys: {}, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff4", + }, + keys: {}, + otherStuff: "otherStuff5", + }); + }); + + it("should set properties for each account", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + FORCE_SET_PASSWORD_REASON_DEFINITION, + "FirstAccount_forceSetPasswordReason", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + MASTER_KEY_HASH_DEFINITION, + "FirstAccount_keyHash", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "FirstAccount", + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + "FirstAccount_masterKeyEncryptedUserKey", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + FORCE_SET_PASSWORD_REASON_DEFINITION, + "SecondAccount_forceSetPasswordReason", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + MASTER_KEY_HASH_DEFINITION, + "SecondAccount_keyHash", + ); + + expect(helper.setToUser).toHaveBeenCalledWith( + "SecondAccount", + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + "SecondAccount_masterKeyEncryptedUserKey", + ); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(postMigrationState(), 55); + sut = new MoveMasterKeyStateToProviderMigrator(54, 55); + }); + + it.each(["FirstAccount", "SecondAccount"])("should null out new values", async (userId) => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + null, + ); + + expect(helper.setToUser).toHaveBeenCalledWith(userId, MASTER_KEY_HASH_DEFINITION, null); + }); + + it("should add explicit value back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + forceSetPasswordReason: "FirstAccount_forceSetPasswordReason", + keyHash: "FirstAccount_keyHash", + otherStuff: "overStuff2", + }, + keys: { + masterKeyEncryptedUserKey: "FirstAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + forceSetPasswordReason: "SecondAccount_forceSetPasswordReason", + keyHash: "SecondAccount_keyHash", + otherStuff: "otherStuff4", + }, + keys: { + masterKeyEncryptedUserKey: "SecondAccount_masterKeyEncryptedUserKey", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should not try to restore values to missing accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts new file mode 100644 index 0000000000..99b22b5661 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/55-move-master-key-state-to-provider.ts @@ -0,0 +1,111 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + keys?: { + masterKeyEncryptedUserKey?: string; + }; + profile?: { + forceSetPasswordReason?: number; + keyHash?: string; + }; +}; + +export const FORCE_SET_PASSWORD_REASON_DEFINITION: KeyDefinitionLike = { + key: "forceSetPasswordReason", + stateDefinition: { + name: "masterPassword", + }, +}; + +export const MASTER_KEY_HASH_DEFINITION: KeyDefinitionLike = { + key: "masterKeyHash", + stateDefinition: { + name: "masterPassword", + }, +}; + +export const MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION: KeyDefinitionLike = { + key: "masterKeyEncryptedUserKey", + stateDefinition: { + name: "masterPassword", + }, +}; + +export class MoveMasterKeyStateToProviderMigrator extends Migrator<54, 55> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const forceSetPasswordReason = account?.profile?.forceSetPasswordReason; + if (forceSetPasswordReason != null) { + await helper.setToUser( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + forceSetPasswordReason, + ); + + delete account.profile.forceSetPasswordReason; + await helper.set(userId, account); + } + + const masterKeyHash = account?.profile?.keyHash; + if (masterKeyHash != null) { + await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, masterKeyHash); + + delete account.profile.keyHash; + await helper.set(userId, account); + } + + const masterKeyEncryptedUserKey = account?.keys?.masterKeyEncryptedUserKey; + if (masterKeyEncryptedUserKey != null) { + await helper.setToUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + masterKeyEncryptedUserKey, + ); + + delete account.keys.masterKeyEncryptedUserKey; + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const forceSetPasswordReason = await helper.getFromUser( + userId, + FORCE_SET_PASSWORD_REASON_DEFINITION, + ); + const masterKeyHash = await helper.getFromUser(userId, MASTER_KEY_HASH_DEFINITION); + const masterKeyEncryptedUserKey = await helper.getFromUser( + userId, + MASTER_KEY_ENCRYPTED_USER_KEY_DEFINITION, + ); + if (account != null) { + if (forceSetPasswordReason != null) { + account.profile = Object.assign(account.profile ?? {}, { + forceSetPasswordReason, + }); + } + if (masterKeyHash != null) { + account.profile = Object.assign(account.profile ?? {}, { + keyHash: masterKeyHash, + }); + } + if (masterKeyEncryptedUserKey != null) { + account.keys = Object.assign(account.keys ?? {}, { + masterKeyEncryptedUserKey, + }); + } + await helper.set(userId, account); + } + + await helper.setToUser(userId, FORCE_SET_PASSWORD_REASON_DEFINITION, null); + await helper.setToUser(userId, MASTER_KEY_HASH_DEFINITION, null); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/56-move-auth-requests.spec.ts b/libs/common/src/state-migrations/migrations/56-move-auth-requests.spec.ts new file mode 100644 index 0000000000..f6bddbce7d --- /dev/null +++ b/libs/common/src/state-migrations/migrations/56-move-auth-requests.spec.ts @@ -0,0 +1,138 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { AuthRequestMigrator } from "./56-move-auth-requests"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + settings: { + otherStuff: "otherStuff2", + approveLoginRequests: true, + }, + otherStuff: "otherStuff3", + adminAuthRequest: { + id: "id1", + privateKey: "privateKey1", + }, + }, + SecondAccount: { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_authRequestLocal_adminAuthRequest: { + id: "id1", + privateKey: "privateKey1", + }, + user_FirstAccount_authRequestLocal_acceptAuthRequests: true, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + settings: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "adminAuthRequest", +}; + +const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "acceptAuthRequests", +}; + +describe("AuthRequestMigrator", () => { + let helper: MockProxy; + let sut: AuthRequestMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 55); + sut = new AuthRequestMigrator(55, 56); + }); + + it("removes the existing adminAuthRequest and approveLoginRequests", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + settings: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).not.toHaveBeenCalledWith("SecondAccount"); + }); + + it("sets the adminAuthRequest and approveLoginRequests under the new key definitions", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, { + id: "id1", + privateKey: "privateKey1", + }); + + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, true); + expect(helper.setToUser).not.toHaveBeenCalledWith("SecondAccount"); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 56); + sut = new AuthRequestMigrator(55, 56); + }); + + it("nulls the new adminAuthRequest and acceptAuthRequests values", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, null); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, null); + }); + + it("sets back the adminAuthRequest and approveLoginRequests under old account object", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + adminAuthRequest: { + id: "id1", + privateKey: "privateKey1", + }, + settings: { + otherStuff: "otherStuff2", + approveLoginRequests: true, + }, + otherStuff: "otherStuff3", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/56-move-auth-requests.ts b/libs/common/src/state-migrations/migrations/56-move-auth-requests.ts new file mode 100644 index 0000000000..4fec3b2de0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/56-move-auth-requests.ts @@ -0,0 +1,104 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type AdminAuthRequestStorable = { + id: string; + privateKey: string; +}; + +type ExpectedAccountType = { + adminAuthRequest?: AdminAuthRequestStorable; + settings?: { + approveLoginRequests?: boolean; + }; +}; + +const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "adminAuthRequest", +}; + +const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = { + stateDefinition: { + name: "authRequestLocal", + }, + key: "acceptAuthRequests", +}; + +export class AuthRequestMigrator extends Migrator<55, 56> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedAccount = false; + + // Migrate admin auth request + const existingAdminAuthRequest = account?.adminAuthRequest; + + if (existingAdminAuthRequest != null) { + await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, existingAdminAuthRequest); + delete account.adminAuthRequest; + updatedAccount = true; + } + + // Migrate approve login requests + const existingApproveLoginRequests = account?.settings?.approveLoginRequests; + + if (existingApproveLoginRequests != null) { + await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, existingApproveLoginRequests); + delete account.settings.approveLoginRequests; + updatedAccount = true; + } + + if (updatedAccount) { + // Save the migrated account + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedAccount = false; + // Rollback admin auth request + const migratedAdminAuthRequest: AdminAuthRequestStorable = await helper.getFromUser( + userId, + ADMIN_AUTH_REQUEST_KEY, + ); + + if (migratedAdminAuthRequest != null) { + account.adminAuthRequest = migratedAdminAuthRequest; + updatedAccount = true; + } + + await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, null); + + // Rollback approve login requests + const migratedAcceptAuthRequest: boolean = await helper.getFromUser( + userId, + ACCEPT_AUTH_REQUESTS_KEY, + ); + + if (migratedAcceptAuthRequest != null) { + account.settings = Object.assign(account.settings ?? {}, { + approveLoginRequests: migratedAcceptAuthRequest, + }); + updatedAccount = true; + } + + await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, null); + + if (updatedAccount) { + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts new file mode 100644 index 0000000000..499cff1c89 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts @@ -0,0 +1,170 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + CIPHERS_DISK, + CIPHERS_DISK_LOCAL, + CipherServiceMigrator, +} from "./57-move-cipher-service-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2"], + user1: { + data: { + localData: { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }, + ciphers: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }, + }, + }, + user2: { + data: { + otherStuff: "otherStuff5", + }, + }, + }; +} + +function rollbackJSON() { + return { + user_user1_ciphersLocal_localData: { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }, + user_user1_ciphers_ciphers: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2"], + user1: { + data: {}, + }, + user2: { + data: { + localData: { + otherStuff: "otherStuff3", + }, + ciphers: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }, + }; +} + +describe("CipherServiceMigrator", () => { + let helper: MockProxy; + let sut: CipherServiceMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 56); + sut = new CipherServiceMigrator(56, 57); + }); + + it("should remove local data and ciphers from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("user1", { + data: {}, + }); + }); + + it("should migrate localData and ciphers to state provider for accounts that have the data", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK_LOCAL, { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }); + expect(helper.setToUser).toHaveBeenCalledWith("user1", CIPHERS_DISK, { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }); + + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK_LOCAL, any()); + expect(helper.setToUser).not.toHaveBeenCalledWith("user2", CIPHERS_DISK, any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 57); + sut = new CipherServiceMigrator(56, 57); + }); + + it.each(["user1", "user2"])("should null out new values", async (userId) => { + await sut.rollback(helper); + expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK_LOCAL, null); + expect(helper.setToUser).toHaveBeenCalledWith(userId, CIPHERS_DISK, null); + }); + + it("should add back localData and ciphers to all accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user1", { + data: { + localData: { + "6865ba55-7966-4d63-b743-b12000d49631": { + lastUsedDate: 1708950970632, + }, + "f895f099-6739-4cca-9d61-b12200d04bfa": { + lastUsedDate: 1709031916943, + }, + }, + ciphers: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, + }, + }, + }); + }); + + it("should not add data back if data wasn't migrated or acct doesn't exist", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("user2", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts new file mode 100644 index 0000000000..e71d889bb7 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts @@ -0,0 +1,79 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountType = { + data: { + localData?: unknown; + ciphers?: unknown; + }; +}; + +export const CIPHERS_DISK_LOCAL: KeyDefinitionLike = { + key: "localData", + stateDefinition: { + name: "ciphersLocal", + }, +}; + +export const CIPHERS_DISK: KeyDefinitionLike = { + key: "ciphers", + stateDefinition: { + name: "ciphers", + }, +}; + +export class CipherServiceMigrator extends Migrator<56, 57> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + let updatedAccount = false; + + //Migrate localData + const localData = account?.data?.localData; + if (localData != null) { + await helper.setToUser(userId, CIPHERS_DISK_LOCAL, localData); + delete account.data.localData; + updatedAccount = true; + } + + //Migrate ciphers + const ciphers = account?.data?.ciphers; + if (ciphers != null) { + await helper.setToUser(userId, CIPHERS_DISK, ciphers); + delete account.data.ciphers; + updatedAccount = true; + } + + if (updatedAccount) { + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + //rollback localData + const localData = await helper.getFromUser(userId, CIPHERS_DISK_LOCAL); + + if (account.data && localData != null) { + account.data.localData = localData; + await helper.set(userId, account); + } + await helper.setToUser(userId, CIPHERS_DISK_LOCAL, null); + + //rollback ciphers + const ciphers = await helper.getFromUser(userId, CIPHERS_DISK); + + if (account.data && ciphers != null) { + account.data.ciphers = ciphers; + await helper.set(userId, account); + } + await helper.setToUser(userId, CIPHERS_DISK, null); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts new file mode 100644 index 0000000000..8d8040e4a0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.spec.ts @@ -0,0 +1,72 @@ +import { MockProxy, any } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + RemoveRefreshTokenMigratedFlagMigrator, +} from "./58-remove-refresh-token-migrated-state-provider-flag"; + +// Represents data in state service pre-migration +function preMigrationJson() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2", "user3"], + + user_user1_token_refreshTokenMigratedToSecureStorage: true, + user_user2_token_refreshTokenMigratedToSecureStorage: false, + }; +} + +function rollbackJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["user1", "user2", "user3"], + }; +} + +describe("RemoveRefreshTokenMigratedFlagMigrator", () => { + let helper: MockProxy; + let sut: RemoveRefreshTokenMigratedFlagMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(preMigrationJson(), 57); + sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58); + }); + + it("should remove refreshTokenMigratedToSecureStorage from state provider for all accounts that have it", async () => { + await sut.migrate(helper); + + expect(helper.removeFromUser).toHaveBeenCalledWith( + "user1", + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + ); + expect(helper.removeFromUser).toHaveBeenCalledWith( + "user2", + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + ); + + expect(helper.removeFromUser).toHaveBeenCalledTimes(2); + + expect(helper.removeFromUser).not.toHaveBeenCalledWith("user3", any()); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 58); + sut = new RemoveRefreshTokenMigratedFlagMigrator(57, 58); + }); + + it("should not add data back and throw IRREVERSIBLE error on call", async () => { + await expect(sut.rollback(helper)).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts new file mode 100644 index 0000000000..9c6d3776fe --- /dev/null +++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts @@ -0,0 +1,34 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +type ExpectedAccountType = NonNullable; + +export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE: KeyDefinitionLike = { + key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key in DeviceTrustCryptoService + stateDefinition: { + name: "token", // matches StateDefinition.name in StateDefinitions + }, +}; + +export class RemoveRefreshTokenMigratedFlagMigrator extends Migrator<57, 58> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const refreshTokenMigratedFlag = await helper.getFromUser( + userId, + REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE, + ); + + if (refreshTokenMigratedFlag != null) { + // Only delete the flag if it exists + await helper.removeFromUser(userId, REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +} diff --git a/libs/common/src/state-migrations/migrator.spec.ts b/libs/common/src/state-migrations/migrator.spec.ts index 3abaa27727..d1189c25ea 100644 --- a/libs/common/src/state-migrations/migrator.spec.ts +++ b/libs/common/src/state-migrations/migrator.spec.ts @@ -26,7 +26,7 @@ describe("migrator default methods", () => { beforeEach(() => { storage = mock(); logService = mock(); - helper = new MigrationHelper(0, storage, logService); + helper = new MigrationHelper(0, storage, logService, "general"); sut = new TestMigrator(0, 1); }); diff --git a/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts new file mode 100644 index 0000000000..edda0dcb2b --- /dev/null +++ b/libs/common/src/tools/generator/abstractions/generator-history.abstraction.ts @@ -0,0 +1,47 @@ +import { Observable } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { GeneratedCredential, GeneratorCategory } from "../history"; + +/** Tracks the history of password generations. + * Each user gets their own store. + */ +export abstract class GeneratorHistoryService { + /** Tracks a new credential. When an item with the same `credential` value + * is found, this method does nothing. When the total number of items exceeds + * {@link HistoryServiceOptions.maxTotal}, then the oldest items exceeding the total + * are deleted. + * @param userId identifies the user storing the credential. + * @param credential stored by the history service. + * @param date when the credential was generated. If this is omitted, then the generator + * uses the date the credential was added to the store instead. + * @returns a promise that completes with the added credential. If the credential + * wasn't added, then the promise completes with `null`. + * @remarks this service is not suitable for use with vault items/ciphers. It models only + * a history of an individually generated credential, while a vault item's history + * may contain several credentials that are better modelled as atomic versions of the + * vault item itself. + */ + track: ( + userId: UserId, + credential: string, + category: GeneratorCategory, + date?: Date, + ) => Promise; + + /** Removes a matching credential from the history service. + * @param userId identifies the user taking the credential. + * @param credential to match in the history service. + * @returns A promise that completes with the credential read. If the credential wasn't found, + * the promise completes with null. + * @remarks this can be used to extract an entry when a credential is stored in the vault. + */ + take: (userId: UserId, credential: string) => Promise; + + /** Lists all credentials for a user. + * @param userId identifies the user listing the credential. + * @remarks This field is eventually consistent with `track` and `take` operations. + * It is not guaranteed to immediately reflect those changes. + */ + credentials$: (userId: UserId) => Observable; +} diff --git a/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts new file mode 100644 index 0000000000..e9fb7e0bb4 --- /dev/null +++ b/libs/common/src/tools/generator/abstractions/generator-navigation.service.abstraction.ts @@ -0,0 +1,42 @@ +import { Observable } from "rxjs"; + +import { UserId } from "../../../types/guid"; +import { GeneratorNavigation } from "../navigation/generator-navigation"; +import { GeneratorNavigationPolicy } from "../navigation/generator-navigation-policy"; + +import { PolicyEvaluator } from "./policy-evaluator.abstraction"; + +/** Loads and stores generator navigational data + */ +export abstract class GeneratorNavigationService { + /** An observable monitoring the options saved to disk. + * The observable updates when the options are saved. + * @param userId: Identifies the user making the request + */ + options$: (userId: UserId) => Observable; + + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable; + + /** An observable monitoring the options used to enforce policy. + * The observable updates when the policy changes. + * @param userId: Identifies the user making the request + */ + evaluator$: ( + userId: UserId, + ) => Observable>; + + /** Enforces the policy on the given options + * @param userId: Identifies the user making the request + * @param options the options to enforce the policy on + * @returns a new instance of the options with the policy enforced + */ + enforcePolicy: (userId: UserId, options: GeneratorNavigation) => Promise; + + /** Saves the navigation options to disk. + * @param userId: Identifies the user making the request + * @param options the options to save + * @returns a promise that resolves when the options are saved + */ + saveOptions: (userId: UserId, options: GeneratorNavigation) => Promise; +} diff --git a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts index f11c1d7300..7cfe320abe 100644 --- a/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator-strategy.abstraction.ts @@ -1,3 +1,5 @@ +import { Observable } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 @@ -15,19 +17,25 @@ export abstract class GeneratorStrategy { */ durableState: (userId: UserId) => SingleUserState; + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable; + /** Identifies the policy enforced by the generator. */ policy: PolicyType; /** Length of time in milliseconds to cache the evaluator */ cache_ms: number; - /** Creates an evaluator from a generator policy. + /** Operator function that converts a policy collection observable to a single + * policy evaluator observable. * @param policy The policy being evaluated. * @returns the policy evaluator. If `policy` is is `null` or `undefined`, * then the evaluator defaults to the application's limits. * @throws when the policy's type does not match the generator's policy type. */ - evaluator: (policy: AdminPolicy) => PolicyEvaluator; + toEvaluator: () => ( + source: Observable, + ) => Observable>; /** Generates credentials from the given options. * @param options The options used to generate the credentials. diff --git a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts index f1820ed707..adb1165552 100644 --- a/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/generator.service.abstraction.ts @@ -21,6 +21,9 @@ export abstract class GeneratorService { */ evaluator$: (userId: UserId) => Observable>; + /** Gets the default options. */ + defaults$: (userId: UserId) => Observable; + /** Enforces the policy on the given options * @param userId: Identifies the user making the request * @param options the options to enforce the policy on diff --git a/libs/common/src/tools/generator/abstractions/index.ts b/libs/common/src/tools/generator/abstractions/index.ts index 03285dd5ff..13dce17d17 100644 --- a/libs/common/src/tools/generator/abstractions/index.ts +++ b/libs/common/src/tools/generator/abstractions/index.ts @@ -1,3 +1,4 @@ +export { GeneratorNavigationService } from "./generator-navigation.service.abstraction"; export { GeneratorService } from "./generator.service.abstraction"; export { GeneratorStrategy } from "./generator-strategy.abstraction"; export { PolicyEvaluator } from "./policy-evaluator.abstraction"; diff --git a/libs/common/src/tools/generator/password/password-generation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts similarity index 69% rename from libs/common/src/tools/generator/password/password-generation.service.abstraction.ts rename to libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts index b8dac20972..b3bd30be5c 100644 --- a/libs/common/src/tools/generator/password/password-generation.service.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/password-generation.service.abstraction.ts @@ -1,8 +1,8 @@ import { PasswordGeneratorPolicyOptions } from "../../../admin-console/models/domain/password-generator-policy-options"; +import { GeneratedPasswordHistory } from "../password/generated-password-history"; +import { PasswordGeneratorOptions } from "../password/password-generator-options"; -import { GeneratedPasswordHistory } from "./generated-password-history"; -import { PasswordGeneratorOptions } from "./password-generator-options"; - +/** @deprecated Use {@link GeneratorService} with a password or passphrase {@link GeneratorStrategy} instead. */ export abstract class PasswordGenerationServiceAbstraction { generatePassword: (options: PasswordGeneratorOptions) => Promise; generatePassphrase: (options: PasswordGeneratorOptions) => Promise; @@ -10,13 +10,8 @@ export abstract class PasswordGenerationServiceAbstraction { enforcePasswordGeneratorPoliciesOnOptions: ( options: PasswordGeneratorOptions, ) => Promise<[PasswordGeneratorOptions, PasswordGeneratorPolicyOptions]>; - getPasswordGeneratorPolicyOptions: () => Promise; saveOptions: (options: PasswordGeneratorOptions) => Promise; getHistory: () => Promise; addHistory: (password: string) => Promise; clear: (userId?: string) => Promise; - normalizeOptions: ( - options: PasswordGeneratorOptions, - enforcedPolicyOptions: PasswordGeneratorPolicyOptions, - ) => void; } diff --git a/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts b/libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts similarity index 75% rename from libs/common/src/tools/generator/username/username-generation.service.abstraction.ts rename to libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts index 05affef0e2..02b25e6113 100644 --- a/libs/common/src/tools/generator/username/username-generation.service.abstraction.ts +++ b/libs/common/src/tools/generator/abstractions/username-generation.service.abstraction.ts @@ -1,5 +1,6 @@ -import { UsernameGeneratorOptions } from "./username-generation-options"; +import { UsernameGeneratorOptions } from "../username/username-generation-options"; +/** @deprecated Use {@link GeneratorService} with a username {@link GeneratorStrategy} instead. */ export abstract class UsernameGenerationServiceAbstraction { generateUsername: (options: UsernameGeneratorOptions) => Promise; generateWord: (options: UsernameGeneratorOptions) => Promise; diff --git a/libs/common/src/tools/generator/default-generator.service.spec.ts b/libs/common/src/tools/generator/default-generator.service.spec.ts index 84b8ff4530..c93aec44d9 100644 --- a/libs/common/src/tools/generator/default-generator.service.spec.ts +++ b/libs/common/src/tools/generator/default-generator.service.spec.ts @@ -4,7 +4,7 @@ */ import { mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, pipe } from "rxjs"; import { FakeSingleUserState, awaitAsync } from "../../../spec"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; @@ -20,12 +20,12 @@ import { PasswordGenerationOptions } from "./password"; import { DefaultGeneratorService } from "."; -function mockPolicyService(config?: { state?: BehaviorSubject }) { +function mockPolicyService(config?: { state?: BehaviorSubject }) { const service = mock(); // FIXME: swap out the mock return value when `getAll$` becomes available - const stateValue = config?.state ?? new BehaviorSubject(null); - service.get$.mockReturnValue(stateValue); + const stateValue = config?.state ?? new BehaviorSubject([null]); + service.getAll$.mockReturnValue(stateValue); // const stateValue = config?.state ?? new BehaviorSubject(null); // service.getAll$.mockReturnValue(stateValue); @@ -37,6 +37,7 @@ function mockGeneratorStrategy(config?: { userState?: SingleUserState; policy?: PolicyType; evaluator?: any; + defaults?: any; }) { const durableState = config?.userState ?? new FakeSingleUserState(SomeUser); @@ -45,8 +46,11 @@ function mockGeneratorStrategy(config?: { // whether they're used properly are guaranteed to test // the value from `config`. durableState: jest.fn(() => durableState), + defaults$: jest.fn(() => new BehaviorSubject(config?.defaults)), policy: config?.policy ?? PolicyType.DisableSend, - evaluator: jest.fn(() => config?.evaluator ?? mock>()), + toEvaluator: jest.fn(() => + pipe(map(() => config?.evaluator ?? mock>())), + ), }); return strategy; @@ -70,6 +74,20 @@ describe("Password generator service", () => { }); }); + describe("defaults$", () => { + it("should retrieve default state from the service", async () => { + const policy = mockPolicyService(); + const defaults = {}; + const strategy = mockGeneratorStrategy({ defaults }); + const service = new DefaultGeneratorService(strategy, policy); + + const result = await firstValueFrom(service.defaults$(SomeUser)); + + expect(strategy.defaults$).toHaveBeenCalledWith(SomeUser); + expect(result).toBe(defaults); + }); + }); + describe("saveOptions()", () => { it("should trigger an options$ update", async () => { const policy = mockPolicyService(); @@ -94,9 +112,7 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); - // FIXME: swap out the expect when `getAll$` becomes available - expect(policy.get$).toHaveBeenCalledWith(PolicyType.PasswordGenerator); - //expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); + expect(policy.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); }); it("should map the policy using the generation strategy", async () => { @@ -112,21 +128,22 @@ describe("Password generator service", () => { it("should update the evaluator when the password generator policy changes", async () => { // set up dependencies - const state = new BehaviorSubject(null); + const state = new BehaviorSubject([null]); const policy = mockPolicyService({ state }); const strategy = mockGeneratorStrategy(); const service = new DefaultGeneratorService(strategy, policy); - // model responses for the observable update + // model responses for the observable update. The map is called multiple times, + // and the array shift ensures reference equality is maintained. const firstEvaluator = mock>(); - strategy.evaluator.mockReturnValueOnce(firstEvaluator); const secondEvaluator = mock>(); - strategy.evaluator.mockReturnValueOnce(secondEvaluator); + const evaluators = [firstEvaluator, secondEvaluator]; + strategy.toEvaluator.mockReturnValueOnce(pipe(map(() => evaluators.shift()))); // act const evaluator$ = service.evaluator$(SomeUser); const firstResult = await firstValueFrom(evaluator$); - state.next(null); + state.next([null]); const secondResult = await firstValueFrom(evaluator$); // assert @@ -142,9 +159,7 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); await firstValueFrom(service.evaluator$(SomeUser)); - // FIXME: swap out the expect when `getAll$` becomes available - expect(policy.get$).toHaveBeenCalledTimes(1); - //expect(policy.getAll$).toHaveBeenCalledTimes(1); + expect(policy.getAll$).toHaveBeenCalledTimes(1); }); it("should cache the password generator policy for each user", async () => { @@ -155,9 +170,8 @@ describe("Password generator service", () => { await firstValueFrom(service.evaluator$(SomeUser)); await firstValueFrom(service.evaluator$(AnotherUser)); - // FIXME: enable this test when `getAll$` becomes available - // expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser); - // expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser); + expect(policy.getAll$).toHaveBeenNthCalledWith(1, PolicyType.PasswordGenerator, SomeUser); + expect(policy.getAll$).toHaveBeenNthCalledWith(2, PolicyType.PasswordGenerator, AnotherUser); }); }); diff --git a/libs/common/src/tools/generator/default-generator.service.ts b/libs/common/src/tools/generator/default-generator.service.ts index 9c884ccefd..7fd794472c 100644 --- a/libs/common/src/tools/generator/default-generator.service.ts +++ b/libs/common/src/tools/generator/default-generator.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, map, share, timer, ReplaySubject, Observable } from "rxjs"; +import { firstValueFrom, share, timer, ReplaySubject, Observable } from "rxjs"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 @@ -21,17 +21,22 @@ export class DefaultGeneratorService implements GeneratorServic private _evaluators$ = new Map>>(); - /** {@link GeneratorService.options$()} */ + /** {@link GeneratorService.options$} */ options$(userId: UserId) { return this.strategy.durableState(userId).state$; } + /** {@link GeneratorService.defaults$} */ + defaults$(userId: UserId) { + return this.strategy.defaults$(userId); + } + /** {@link GeneratorService.saveOptions} */ async saveOptions(userId: UserId, options: Options): Promise { await this.strategy.durableState(userId).update(() => options); } - /** {@link GeneratorService.evaluator$()} */ + /** {@link GeneratorService.evaluator$} */ evaluator$(userId: UserId) { let evaluator$ = this._evaluators$.get(userId); @@ -44,14 +49,12 @@ export class DefaultGeneratorService implements GeneratorServic } private createEvaluator(userId: UserId) { - // FIXME: when it becomes possible to get a user-specific policy observable - // (`getAll$`) update this code to call it instead of `get$`. - const policies$ = this.policy.get$(this.strategy.policy); + const evaluator$ = this.policy.getAll$(this.strategy.policy, userId).pipe( + // create the evaluator from the policies + this.strategy.toEvaluator(), - // cache evaluator in a replay subject to amortize creation cost - // and reduce GC pressure. - const evaluator$ = policies$.pipe( - map((policy) => this.strategy.evaluator(policy)), + // cache evaluator in a replay subject to amortize creation cost + // and reduce GC pressure. share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(this.strategy.cache_ms), @@ -61,7 +64,7 @@ export class DefaultGeneratorService implements GeneratorServic return evaluator$; } - /** {@link GeneratorService.enforcePolicy()} */ + /** {@link GeneratorService.enforcePolicy} */ async enforcePolicy(userId: UserId, options: Options): Promise { const policy = await firstValueFrom(this.evaluator$(userId)); const evaluated = policy.applyPolicy(options); diff --git a/libs/common/src/tools/generator/generator-options.ts b/libs/common/src/tools/generator/generator-options.ts index 4f8eb293ab..d3d08025fa 100644 --- a/libs/common/src/tools/generator/generator-options.ts +++ b/libs/common/src/tools/generator/generator-options.ts @@ -1,3 +1,5 @@ -export type GeneratorOptions = { - type?: "password" | "username"; -}; +// this export provided solely for backwards compatibility +export { + /** @deprecated use `GeneratorNavigation` from './navigation' instead. */ + GeneratorNavigation as GeneratorOptions, +} from "./navigation/generator-navigation"; diff --git a/libs/common/src/tools/generator/generator-type.ts b/libs/common/src/tools/generator/generator-type.ts new file mode 100644 index 0000000000..f17eeb9c92 --- /dev/null +++ b/libs/common/src/tools/generator/generator-type.ts @@ -0,0 +1,2 @@ +/** The kind of credential being generated. */ +export type GeneratorType = "password" | "passphrase" | "username"; diff --git a/libs/common/src/tools/generator/history/generated-credential.spec.ts b/libs/common/src/tools/generator/history/generated-credential.spec.ts new file mode 100644 index 0000000000..170030bad1 --- /dev/null +++ b/libs/common/src/tools/generator/history/generated-credential.spec.ts @@ -0,0 +1,58 @@ +import { GeneratorCategory, GeneratedCredential } from "./"; + +describe("GeneratedCredential", () => { + describe("constructor", () => { + it("assigns credential", () => { + const result = new GeneratedCredential("example", "passphrase", new Date(100)); + + expect(result.credential).toEqual("example"); + }); + + it("assigns category", () => { + const result = new GeneratedCredential("example", "passphrase", new Date(100)); + + expect(result.category).toEqual("passphrase"); + }); + + it("passes through date parameters", () => { + const result = new GeneratedCredential("example", "password", new Date(100)); + + expect(result.generationDate).toEqual(new Date(100)); + }); + + it("converts numeric dates to Dates", () => { + const result = new GeneratedCredential("example", "password", 100); + + expect(result.generationDate).toEqual(new Date(100)); + }); + }); + + it("toJSON converts from a credential into a JSON object", () => { + const credential = new GeneratedCredential("example", "password", new Date(100)); + + const result = credential.toJSON(); + + expect(result).toEqual({ + credential: "example", + category: "password" as GeneratorCategory, + generationDate: 100, + }); + }); + + it("fromJSON converts Json objects into credentials", () => { + const jsonValue = { + credential: "example", + category: "password" as GeneratorCategory, + generationDate: 100, + }; + + const result = GeneratedCredential.fromJSON(jsonValue); + + expect(result).toBeInstanceOf(GeneratedCredential); + expect(result).toEqual({ + credential: "example", + category: "password", + generationDate: new Date(100), + }); + }); +}); diff --git a/libs/common/src/tools/generator/history/generated-credential.ts b/libs/common/src/tools/generator/history/generated-credential.ts new file mode 100644 index 0000000000..59a9623bf7 --- /dev/null +++ b/libs/common/src/tools/generator/history/generated-credential.ts @@ -0,0 +1,47 @@ +import { Jsonify } from "type-fest"; + +import { GeneratorCategory } from "./options"; + +/** A credential generation result */ +export class GeneratedCredential { + /** + * Instantiates a generated credential + * @param credential The value of the generated credential (e.g. a password) + * @param category The kind of credential + * @param generationDate The date that the credential was generated. + * Numeric values should are interpreted using {@link Date.valueOf} + * semantics. + */ + constructor( + readonly credential: string, + readonly category: GeneratorCategory, + generationDate: Date | number, + ) { + if (typeof generationDate === "number") { + this.generationDate = new Date(generationDate); + } else { + this.generationDate = generationDate; + } + } + + /** The date that the credential was generated */ + generationDate: Date; + + /** Constructs a credential from its `toJSON` representation */ + static fromJSON(jsonValue: Jsonify) { + return new GeneratedCredential( + jsonValue.credential, + jsonValue.category, + jsonValue.generationDate, + ); + } + + /** Serializes a credential to a JSON-compatible object */ + toJSON() { + return { + credential: this.credential, + category: this.category, + generationDate: this.generationDate.valueOf(), + }; + } +} diff --git a/libs/common/src/tools/generator/history/index.ts b/libs/common/src/tools/generator/history/index.ts new file mode 100644 index 0000000000..1952a849af --- /dev/null +++ b/libs/common/src/tools/generator/history/index.ts @@ -0,0 +1,2 @@ +export { GeneratorCategory } from "./options"; +export { GeneratedCredential } from "./generated-credential"; diff --git a/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts b/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts new file mode 100644 index 0000000000..57dde51fc1 --- /dev/null +++ b/libs/common/src/tools/generator/history/local-generator-history.service.spec.ts @@ -0,0 +1,198 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../spec"; +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EncString } from "../../../platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../../types/csprng"; +import { UserId } from "../../../types/guid"; +import { UserKey } from "../../../types/key"; + +import { LocalGeneratorHistoryService } from "./local-generator-history.service"; + +const SomeUser = "SomeUser" as UserId; +const AnotherUser = "AnotherUser" as UserId; + +describe("LocalGeneratorHistoryService", () => { + const encryptService = mock(); + const keyService = mock(); + const userKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey; + + beforeEach(() => { + encryptService.encrypt.mockImplementation((p) => Promise.resolve(p as unknown as EncString)); + encryptService.decryptToUtf8.mockImplementation((c) => Promise.resolve(c.encryptedString)); + keyService.getUserKey.mockImplementation(() => Promise.resolve(userKey)); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("credential$", () => { + it("returns an empty list when no credentials are stored", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + const result = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toEqual([]); + }); + }); + + describe("track", () => { + it("stores a password", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "password"); + await awaitAsync(); + const [result] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toMatchObject({ credential: "example", category: "password" }); + }); + + it("stores a passphrase", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "passphrase"); + await awaitAsync(); + const [result] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toMatchObject({ credential: "example", category: "passphrase" }); + }); + + it("stores a specific date when one is provided", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "password", new Date(100)); + await awaitAsync(); + const [result] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(result).toEqual({ + credential: "example", + category: "password", + generationDate: new Date(100), + }); + }); + + it("skips storing a credential when it's already stored (ignores category)", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", "password"); + await history.track(SomeUser, "example", "passphrase"); + await awaitAsync(); + const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(firstResult).toMatchObject({ credential: "example", category: "password" }); + expect(secondResult).toBeUndefined(); + }); + + it("stores multiple credentials when the credential value is different", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + await history.track(SomeUser, "secondResult", "password"); + await history.track(SomeUser, "firstResult", "password"); + await awaitAsync(); + const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(firstResult).toMatchObject({ credential: "firstResult", category: "password" }); + expect(secondResult).toMatchObject({ credential: "secondResult", category: "password" }); + }); + + it("removes history items exceeding maxTotal configuration", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider, { + maxTotal: 1, + }); + + await history.track(SomeUser, "removed result", "password"); + await history.track(SomeUser, "example", "password"); + await awaitAsync(); + const [firstResult, secondResult] = await firstValueFrom(history.credentials$(SomeUser)); + + expect(firstResult).toMatchObject({ credential: "example", category: "password" }); + expect(secondResult).toBeUndefined(); + }); + + it("stores history items in per-user collections", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider, { + maxTotal: 1, + }); + + await history.track(SomeUser, "some user example", "password"); + await history.track(AnotherUser, "another user example", "password"); + await awaitAsync(); + const [someFirstResult, someSecondResult] = await firstValueFrom( + history.credentials$(SomeUser), + ); + const [anotherFirstResult, anotherSecondResult] = await firstValueFrom( + history.credentials$(AnotherUser), + ); + + expect(someFirstResult).toMatchObject({ + credential: "some user example", + category: "password", + }); + expect(someSecondResult).toBeUndefined(); + expect(anotherFirstResult).toMatchObject({ + credential: "another user example", + category: "password", + }); + expect(anotherSecondResult).toBeUndefined(); + }); + }); + + describe("take", () => { + it("returns null when there are no credentials stored", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + + const result = await history.take(SomeUser, "example"); + + expect(result).toBeNull(); + }); + + it("returns null when the credential wasn't found", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + await history.track(SomeUser, "example", "password"); + + const result = await history.take(SomeUser, "not found"); + + expect(result).toBeNull(); + }); + + it("returns a matching credential", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + await history.track(SomeUser, "example", "password"); + + const result = await history.take(SomeUser, "example"); + + expect(result).toMatchObject({ + credential: "example", + category: "password", + }); + }); + + it("removes a matching credential", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); + await history.track(SomeUser, "example", "password"); + + await history.take(SomeUser, "example"); + await awaitAsync(); + const results = await firstValueFrom(history.credentials$(SomeUser)); + + expect(results).toEqual([]); + }); + }); +}); diff --git a/libs/common/src/tools/generator/history/local-generator-history.service.ts b/libs/common/src/tools/generator/history/local-generator-history.service.ts new file mode 100644 index 0000000000..3a65890c50 --- /dev/null +++ b/libs/common/src/tools/generator/history/local-generator-history.service.ts @@ -0,0 +1,116 @@ +import { map } from "rxjs"; + +import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { SingleUserState, StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { GeneratorHistoryService } from "../abstractions/generator-history.abstraction"; +import { GENERATOR_HISTORY } from "../key-definitions"; +import { PaddedDataPacker } from "../state/padded-data-packer"; +import { SecretState } from "../state/secret-state"; +import { UserKeyEncryptor } from "../state/user-key-encryptor"; + +import { GeneratedCredential } from "./generated-credential"; +import { GeneratorCategory, HistoryServiceOptions } from "./options"; + +const OPTIONS_FRAME_SIZE = 2048; + +/** Tracks the history of password generations local to a device. + * {@link GeneratorHistoryService} + */ +export class LocalGeneratorHistoryService extends GeneratorHistoryService { + constructor( + private readonly encryptService: EncryptService, + private readonly keyService: CryptoService, + private readonly stateProvider: StateProvider, + private readonly options: HistoryServiceOptions = { maxTotal: 100 }, + ) { + super(); + } + + private _credentialStates = new Map>(); + + /** {@link GeneratorHistoryService.track} */ + track = async (userId: UserId, credential: string, category: GeneratorCategory, date?: Date) => { + const state = this.getCredentialState(userId); + let result: GeneratedCredential = null; + + await state.update( + (credentials) => { + credentials = credentials ?? []; + + // add the result + result = new GeneratedCredential(credential, category, date ?? Date.now()); + credentials.unshift(result); + + // trim history + const removeAt = Math.max(0, this.options.maxTotal); + credentials.splice(removeAt, Infinity); + + return credentials; + }, + { + shouldUpdate: (credentials) => + credentials?.some((f) => f.credential !== credential) ?? true, + }, + ); + + return result; + }; + + /** {@link GeneratorHistoryService.take} */ + take = async (userId: UserId, credential: string) => { + const state = this.getCredentialState(userId); + let credentialIndex: number; + let result: GeneratedCredential = null; + + await state.update( + (credentials) => { + credentials = credentials ?? []; + + [result] = credentials.splice(credentialIndex, 1); + return credentials; + }, + { + shouldUpdate: (credentials) => { + credentialIndex = credentials?.findIndex((f) => f.credential === credential) ?? -1; + return credentialIndex >= 0; + }, + }, + ); + + return result; + }; + + /** {@link GeneratorHistoryService.credentials$} */ + credentials$ = (userId: UserId) => { + return this.getCredentialState(userId).state$.pipe(map((credentials) => credentials ?? [])); + }; + + private getCredentialState(userId: UserId) { + let state = this._credentialStates.get(userId); + + if (!state) { + state = this.createSecretState(userId); + this._credentialStates.set(userId, state); + } + + return state; + } + + private createSecretState(userId: UserId) { + // construct the encryptor + const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE); + const encryptor = new UserKeyEncryptor(this.encryptService, this.keyService, packer); + + const state = SecretState.from< + GeneratedCredential[], + number, + GeneratedCredential, + Record, + GeneratedCredential + >(userId, GENERATOR_HISTORY, this.stateProvider, encryptor); + + return state; + } +} diff --git a/libs/common/src/tools/generator/history/options.ts b/libs/common/src/tools/generator/history/options.ts new file mode 100644 index 0000000000..53716ec33a --- /dev/null +++ b/libs/common/src/tools/generator/history/options.ts @@ -0,0 +1,10 @@ +/** Kinds of credentials that can be stored by the history service */ +export type GeneratorCategory = "password" | "passphrase"; + +/** Configuration options for the history service */ +export type HistoryServiceOptions = { + /** Total number of records retained across all types. + * @remarks Setting this to 0 or less disables history completely. + * */ + maxTotal: number; +}; diff --git a/libs/common/src/tools/generator/key-definition.spec.ts b/libs/common/src/tools/generator/key-definition.spec.ts index 735377a5ba..9cbbc44e14 100644 --- a/libs/common/src/tools/generator/key-definition.spec.ts +++ b/libs/common/src/tools/generator/key-definition.spec.ts @@ -1,5 +1,4 @@ import { - ENCRYPTED_HISTORY, EFF_USERNAME_SETTINGS, CATCHALL_SETTINGS, SUBADDRESS_SETTINGS, @@ -11,9 +10,18 @@ import { FASTMAIL_FORWARDER, DUCK_DUCK_GO_FORWARDER, ADDY_IO_FORWARDER, + GENERATOR_SETTINGS, } from "./key-definitions"; describe("Key definitions", () => { + describe("GENERATOR_SETTINGS", () => { + it("should pass through deserialization", () => { + const value = {}; + const result = GENERATOR_SETTINGS.deserializer(value); + expect(result).toBe(value); + }); + }); + describe("PASSWORD_SETTINGS", () => { it("should pass through deserialization", () => { const value = {}; @@ -32,7 +40,7 @@ describe("Key definitions", () => { describe("EFF_USERNAME_SETTINGS", () => { it("should pass through deserialization", () => { - const value = {}; + const value = { website: null as string }; const result = EFF_USERNAME_SETTINGS.deserializer(value); expect(result).toBe(value); }); @@ -40,7 +48,7 @@ describe("Key definitions", () => { describe("CATCHALL_SETTINGS", () => { it("should pass through deserialization", () => { - const value = {}; + const value = { website: null as string }; const result = CATCHALL_SETTINGS.deserializer(value); expect(result).toBe(value); }); @@ -48,7 +56,7 @@ describe("Key definitions", () => { describe("SUBADDRESS_SETTINGS", () => { it("should pass through deserialization", () => { - const value = {}; + const value = { website: null as string }; const result = SUBADDRESS_SETTINGS.deserializer(value); expect(result).toBe(value); }); @@ -101,12 +109,4 @@ describe("Key definitions", () => { expect(result).toBe(value); }); }); - - describe("ENCRYPTED_HISTORY", () => { - it("should pass through deserialization", () => { - const value = {}; - const result = ENCRYPTED_HISTORY.deserializer(value as any); - expect(result).toBe(value); - }); - }); }); diff --git a/libs/common/src/tools/generator/key-definitions.ts b/libs/common/src/tools/generator/key-definitions.ts index bb7c4e8a08..074df48468 100644 --- a/libs/common/src/tools/generator/key-definitions.ts +++ b/libs/common/src/tools/generator/key-definitions.ts @@ -1,8 +1,11 @@ -import { GENERATOR_DISK, KeyDefinition } from "../../platform/state"; +import { GENERATOR_DISK, GENERATOR_MEMORY, UserKeyDefinition } from "../../platform/state"; +import { GeneratedCredential } from "./history/generated-credential"; +import { GeneratorNavigation } from "./navigation/generator-navigation"; import { PassphraseGenerationOptions } from "./passphrase/passphrase-generation-options"; -import { GeneratedPasswordHistory } from "./password/generated-password-history"; import { PasswordGenerationOptions } from "./password/password-generation-options"; +import { SecretClassifier } from "./state/secret-classifier"; +import { SecretKeyDefinition } from "./state/secret-key-definition"; import { CatchallGenerationOptions } from "./username/catchall-generator-options"; import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; import { @@ -14,103 +17,132 @@ import { import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; /** plaintext password generation options */ -export const PASSWORD_SETTINGS = new KeyDefinition( +export const GENERATOR_SETTINGS = new UserKeyDefinition( + GENERATOR_MEMORY, + "generatorSettings", + { + deserializer: (value) => value, + clearOn: ["lock", "logout"], + }, +); + +/** plaintext password generation options */ +export const PASSWORD_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "passwordGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext passphrase generation options */ -export const PASSPHRASE_SETTINGS = new KeyDefinition( +export const PASSPHRASE_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "passphraseGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); /** plaintext username generation options */ -export const EFF_USERNAME_SETTINGS = new KeyDefinition( +export const EFF_USERNAME_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "effUsernameGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); -/** catchall email generation options */ -export const CATCHALL_SETTINGS = new KeyDefinition( +/** plaintext configuration for a domain catch-all address. */ +export const CATCHALL_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "catchallGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); -/** email subaddress generation options */ -export const SUBADDRESS_SETTINGS = new KeyDefinition( +/** plaintext configuration for an email subaddress. */ +export const SUBADDRESS_SETTINGS = new UserKeyDefinition( GENERATOR_DISK, "subaddressGeneratorSettings", { deserializer: (value) => value, + clearOn: [], }, ); -export const ADDY_IO_FORWARDER = new KeyDefinition( +/** backing store configuration for {@link Forwarders.AddyIo} */ +export const ADDY_IO_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "addyIoForwarder", { deserializer: (value) => value, + clearOn: [], }, ); -export const DUCK_DUCK_GO_FORWARDER = new KeyDefinition( +/** backing store configuration for {@link Forwarders.DuckDuckGo} */ +export const DUCK_DUCK_GO_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "duckDuckGoForwarder", { deserializer: (value) => value, + clearOn: [], }, ); -export const FASTMAIL_FORWARDER = new KeyDefinition( +/** backing store configuration for {@link Forwarders.FastMail} */ +export const FASTMAIL_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "fastmailForwarder", { deserializer: (value) => value, + clearOn: [], }, ); -export const FIREFOX_RELAY_FORWARDER = new KeyDefinition( +/** backing store configuration for {@link Forwarders.FireFoxRelay} */ +export const FIREFOX_RELAY_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "firefoxRelayForwarder", { deserializer: (value) => value, + clearOn: [], }, ); -export const FORWARD_EMAIL_FORWARDER = new KeyDefinition( +/** backing store configuration for {@link Forwarders.ForwardEmail} */ +export const FORWARD_EMAIL_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "forwardEmailForwarder", { deserializer: (value) => value, + clearOn: [], }, ); -export const SIMPLE_LOGIN_FORWARDER = new KeyDefinition( +/** backing store configuration for {@link forwarders.SimpleLogin} */ +export const SIMPLE_LOGIN_FORWARDER = new UserKeyDefinition( GENERATOR_DISK, "simpleLoginForwarder", { deserializer: (value) => value, + clearOn: [], }, ); /** encrypted password generation history */ -export const ENCRYPTED_HISTORY = new KeyDefinition( +export const GENERATOR_HISTORY = SecretKeyDefinition.array( GENERATOR_DISK, - "passwordGeneratorHistory", + "localGeneratorHistory", + SecretClassifier.allSecret(), { - deserializer: (value) => value, + deserializer: GeneratedCredential.fromJSON, + clearOn: ["logout"], }, ); diff --git a/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts b/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts new file mode 100644 index 0000000000..093c68b3e8 --- /dev/null +++ b/libs/common/src/tools/generator/legacy-password-generation.service.spec.ts @@ -0,0 +1,470 @@ +/** + * include structuredClone in test environment. + * @jest-environment ../../../shared/test.environment.ts + */ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { mockAccountServiceWith } from "../../../spec"; +import { UserId } from "../../types/guid"; + +import { GeneratorNavigationService, GeneratorService } from "./abstractions"; +import { LegacyPasswordGenerationService } from "./legacy-password-generation.service"; +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation"; +import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator"; +import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy"; +import { + DefaultPassphraseGenerationOptions, + PassphraseGenerationOptions, + PassphraseGeneratorOptionsEvaluator, + PassphraseGeneratorPolicy, +} from "./passphrase"; +import { DisabledPassphraseGeneratorPolicy } from "./passphrase/passphrase-generator-policy"; +import { + DefaultPasswordGenerationOptions, + PasswordGenerationOptions, + PasswordGeneratorOptions, + PasswordGeneratorOptionsEvaluator, + PasswordGeneratorPolicy, +} from "./password"; +import { DisabledPasswordGeneratorPolicy } from "./password/password-generator-policy"; + +const SomeUser = "some user" as UserId; + +function createPassphraseGenerator( + options: PassphraseGenerationOptions = {}, + policy: PassphraseGeneratorPolicy = DisabledPassphraseGeneratorPolicy, +) { + let savedOptions = options; + const generator = mock>({ + evaluator$(id: UserId) { + const evaluator = new PassphraseGeneratorOptionsEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultPassphraseGenerationOptions); + }, + saveOptions(userId, options) { + savedOptions = options; + return Promise.resolve(); + }, + }); + + return generator; +} + +function createPasswordGenerator( + options: PasswordGenerationOptions = {}, + policy: PasswordGeneratorPolicy = DisabledPasswordGeneratorPolicy, +) { + let savedOptions = options; + const generator = mock>({ + evaluator$(id: UserId) { + const evaluator = new PasswordGeneratorOptionsEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultPasswordGenerationOptions); + }, + saveOptions(userId, options) { + savedOptions = options; + return Promise.resolve(); + }, + }); + + return generator; +} + +function createNavigationGenerator( + options: GeneratorNavigation = {}, + policy: GeneratorNavigationPolicy = {}, +) { + let savedOptions = options; + const generator = mock({ + evaluator$(id: UserId) { + const evaluator = new GeneratorNavigationEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultGeneratorNavigation); + }, + saveOptions(userId, options) { + savedOptions = options; + return Promise.resolve(); + }, + }); + + return generator; +} + +describe("LegacyPasswordGenerationService", () => { + // NOTE: in all tests, `null` constructor arguments are not used by the test. + // They're set to `null` to avoid setting up unnecessary mocks. + + describe("generatePassword", () => { + it("invokes the inner password generator to generate passwords", async () => { + const innerPassword = createPasswordGenerator(); + const generator = new LegacyPasswordGenerationService(null, null, innerPassword, null); + const options = { type: "password" } as PasswordGeneratorOptions; + + await generator.generatePassword(options); + + expect(innerPassword.generate).toHaveBeenCalledWith(options); + }); + + it("invokes the inner passphrase generator to generate passphrases", async () => { + const innerPassphrase = createPassphraseGenerator(); + const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase); + const options = { type: "passphrase" } as PasswordGeneratorOptions; + + await generator.generatePassword(options); + + expect(innerPassphrase.generate).toHaveBeenCalledWith(options); + }); + }); + + describe("generatePassphrase", () => { + it("invokes the inner passphrase generator", async () => { + const innerPassphrase = createPassphraseGenerator(); + const generator = new LegacyPasswordGenerationService(null, null, null, innerPassphrase); + const options = {} as PasswordGeneratorOptions; + + await generator.generatePassphrase(options); + + expect(innerPassphrase.generate).toHaveBeenCalledWith(options); + }); + }); + + describe("getOptions", () => { + it("combines options from its inner services", async () => { + const innerPassword = createPasswordGenerator({ + length: 29, + minLength: 20, + ambiguous: false, + uppercase: true, + minUppercase: 1, + lowercase: false, + minLowercase: 2, + number: true, + minNumber: 3, + special: false, + minSpecial: 4, + }); + const innerPassphrase = createPassphraseGenerator({ + numWords: 10, + wordSeparator: "-", + capitalize: true, + includeNumber: false, + }); + const navigation = createNavigationGenerator({ + type: "passphrase", + username: "word", + forwarder: "simplelogin", + }); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.getOptions(); + + expect(result).toEqual({ + type: "passphrase", + username: "word", + forwarder: "simplelogin", + length: 29, + minLength: 20, + ambiguous: false, + uppercase: true, + minUppercase: 1, + lowercase: false, + minLowercase: 2, + number: true, + minNumber: 3, + special: false, + minSpecial: 4, + numWords: 10, + wordSeparator: "-", + capitalize: true, + includeNumber: false, + }); + }); + + it("sets default options when an inner service lacks a value", async () => { + const innerPassword = createPasswordGenerator(null); + const innerPassphrase = createPassphraseGenerator(null); + const navigation = createNavigationGenerator(null); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.getOptions(); + + expect(result).toEqual({ + ...DefaultGeneratorNavigation, + ...DefaultPassphraseGenerationOptions, + ...DefaultPasswordGenerationOptions, + }); + }); + + it("combines policies from its inner services", async () => { + const innerPassword = createPasswordGenerator( + {}, + { + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + }, + ); + const innerPassphrase = createPassphraseGenerator( + {}, + { + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }, + ); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator( + {}, + { + defaultType: "password", + }, + ); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [, policy] = await generator.getOptions(); + + expect(policy).toEqual({ + defaultType: "password", + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }); + }); + }); + + describe("enforcePasswordGeneratorPoliciesOnOptions", () => { + it("returns its options parameter with password policy applied", async () => { + const innerPassword = createPasswordGenerator( + {}, + { + minLength: 15, + numberCount: 5, + specialCount: 5, + useUppercase: true, + useLowercase: true, + useNumbers: true, + useSpecial: true, + }, + ); + const innerPassphrase = createPassphraseGenerator(); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator(); + const options = { + type: "password" as const, + }; + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); + + expect(result).toBe(options); + expect(result).toMatchObject({ + length: 15, + minLength: 15, + minLowercase: 1, + minNumber: 5, + minUppercase: 1, + minSpecial: 5, + uppercase: true, + lowercase: true, + number: true, + special: true, + }); + }); + + it("returns its options parameter with passphrase policy applied", async () => { + const innerPassword = createPasswordGenerator(); + const innerPassphrase = createPassphraseGenerator( + {}, + { + minNumberWords: 5, + capitalize: true, + includeNumber: true, + }, + ); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator(); + const options = { + type: "passphrase" as const, + }; + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [result] = await generator.enforcePasswordGeneratorPoliciesOnOptions(options); + + expect(result).toBe(options); + expect(result).toMatchObject({ + numWords: 5, + capitalize: true, + includeNumber: true, + }); + }); + + it("returns the applied policy", async () => { + const innerPassword = createPasswordGenerator( + {}, + { + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + }, + ); + const innerPassphrase = createPassphraseGenerator( + {}, + { + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }, + ); + const accountService = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator( + {}, + { + defaultType: "password", + }, + ); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + + const [, policy] = await generator.enforcePasswordGeneratorPoliciesOnOptions({}); + + expect(policy).toEqual({ + defaultType: "password", + minLength: 20, + numberCount: 10, + specialCount: 11, + useUppercase: true, + useLowercase: false, + useNumbers: true, + useSpecial: false, + minNumberWords: 5, + capitalize: true, + includeNumber: false, + }); + }); + }); + + describe("saveOptions", () => { + it("loads saved password options", async () => { + const innerPassword = createPasswordGenerator(); + const innerPassphrase = createPassphraseGenerator(); + const navigation = createNavigationGenerator(); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + const options = { + type: "password" as const, + username: "word" as const, + forwarder: "simplelogin" as const, + length: 29, + minLength: 20, + ambiguous: false, + uppercase: true, + minUppercase: 1, + lowercase: false, + minLowercase: 2, + number: true, + minNumber: 3, + special: false, + minSpecial: 4, + }; + await generator.saveOptions(options); + + const [result] = await generator.getOptions(); + + expect(result).toMatchObject(options); + }); + + it("loads saved passphrase options", async () => { + const innerPassword = createPasswordGenerator(); + const innerPassphrase = createPassphraseGenerator(); + const navigation = createNavigationGenerator(); + const accountService = mockAccountServiceWith(SomeUser); + const generator = new LegacyPasswordGenerationService( + accountService, + navigation, + innerPassword, + innerPassphrase, + ); + const options = { + type: "passphrase" as const, + username: "word" as const, + forwarder: "simplelogin" as const, + numWords: 10, + wordSeparator: "-", + capitalize: true, + includeNumber: false, + }; + await generator.saveOptions(options); + + const [result] = await generator.getOptions(); + + expect(result).toMatchObject(options); + }); + }); +}); diff --git a/libs/common/src/tools/generator/legacy-password-generation.service.ts b/libs/common/src/tools/generator/legacy-password-generation.service.ts new file mode 100644 index 0000000000..0b429b356b --- /dev/null +++ b/libs/common/src/tools/generator/legacy-password-generation.service.ts @@ -0,0 +1,184 @@ +import { concatMap, zip, map, firstValueFrom } from "rxjs"; + +import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; +import { PasswordGeneratorPolicyOptions } from "../../admin-console/models/domain/password-generator-policy-options"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { StateProvider } from "../../platform/state"; + +import { GeneratorService, GeneratorNavigationService } from "./abstractions"; +import { PasswordGenerationServiceAbstraction } from "./abstractions/password-generation.service.abstraction"; +import { DefaultGeneratorService } from "./default-generator.service"; +import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service"; +import { + PassphraseGenerationOptions, + PassphraseGeneratorPolicy, + PassphraseGeneratorStrategy, +} from "./passphrase"; +import { + PasswordGenerationOptions, + PasswordGenerationService, + PasswordGeneratorOptions, + PasswordGeneratorPolicy, + PasswordGeneratorStrategy, +} from "./password"; + +export function legacyPasswordGenerationServiceFactory( + cryptoService: CryptoService, + policyService: PolicyService, + accountService: AccountService, + stateProvider: StateProvider, +): PasswordGenerationServiceAbstraction { + // FIXME: Once the password generation service is replaced with this service + // in the clients, factor out the deprecated service in its entirety. + const deprecatedService = new PasswordGenerationService(cryptoService, null, null); + + const passwords = new DefaultGeneratorService( + new PasswordGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const passphrases = new DefaultGeneratorService( + new PassphraseGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService); + + return new LegacyPasswordGenerationService(accountService, navigation, passwords, passphrases); +} + +/** Adapts the generator 2.0 design to 1.0 angular services. */ +export class LegacyPasswordGenerationService implements PasswordGenerationServiceAbstraction { + constructor( + private readonly accountService: AccountService, + private readonly navigation: GeneratorNavigationService, + private readonly passwords: GeneratorService< + PasswordGenerationOptions, + PasswordGeneratorPolicy + >, + private readonly passphrases: GeneratorService< + PassphraseGenerationOptions, + PassphraseGeneratorPolicy + >, + ) {} + + generatePassword(options: PasswordGeneratorOptions) { + if (options.type === "password") { + return this.passwords.generate(options); + } else { + return this.passphrases.generate(options); + } + } + + generatePassphrase(options: PasswordGeneratorOptions) { + return this.passphrases.generate(options); + } + + async getOptions() { + const options$ = this.accountService.activeAccount$.pipe( + concatMap((activeUser) => + zip( + this.passwords.options$(activeUser.id), + this.passwords.defaults$(activeUser.id), + this.passwords.evaluator$(activeUser.id), + this.passphrases.options$(activeUser.id), + this.passphrases.defaults$(activeUser.id), + this.passphrases.evaluator$(activeUser.id), + this.navigation.options$(activeUser.id), + this.navigation.defaults$(activeUser.id), + this.navigation.evaluator$(activeUser.id), + ), + ), + map( + ([ + passwordOptions, + passwordDefaults, + passwordEvaluator, + passphraseOptions, + passphraseDefaults, + passphraseEvaluator, + generatorOptions, + generatorDefaults, + generatorEvaluator, + ]) => { + const options: PasswordGeneratorOptions = Object.assign( + {}, + passwordOptions ?? passwordDefaults, + passphraseOptions ?? passphraseDefaults, + generatorOptions ?? generatorDefaults, + ); + + const policy = Object.assign( + new PasswordGeneratorPolicyOptions(), + passwordEvaluator.policy, + passphraseEvaluator.policy, + generatorEvaluator.policy, + ); + + return [options, policy] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions]; + }, + ), + ); + + const options = await firstValueFrom(options$); + return options; + } + + async enforcePasswordGeneratorPoliciesOnOptions(options: PasswordGeneratorOptions) { + const options$ = this.accountService.activeAccount$.pipe( + concatMap((activeUser) => + zip( + this.passwords.evaluator$(activeUser.id), + this.passphrases.evaluator$(activeUser.id), + this.navigation.evaluator$(activeUser.id), + ), + ), + map(([passwordEvaluator, passphraseEvaluator, navigationEvaluator]) => { + const policy = Object.assign( + new PasswordGeneratorPolicyOptions(), + passwordEvaluator.policy, + passphraseEvaluator.policy, + navigationEvaluator.policy, + ); + + const navigationApplied = navigationEvaluator.applyPolicy(options); + const navigationSanitized = { + ...options, + ...navigationEvaluator.sanitize(navigationApplied), + }; + if (options.type === "password") { + const applied = passwordEvaluator.applyPolicy(navigationSanitized); + const sanitized = passwordEvaluator.sanitize(applied); + return [sanitized, policy]; + } else { + const applied = passphraseEvaluator.applyPolicy(navigationSanitized); + const sanitized = passphraseEvaluator.sanitize(applied); + return [sanitized, policy]; + } + }), + ); + + const [sanitized, policy] = await firstValueFrom(options$); + return [ + // callers assume this function updates the options parameter + Object.assign(options, sanitized), + policy, + ] as [PasswordGenerationOptions, PasswordGeneratorPolicyOptions]; + } + + async saveOptions(options: PasswordGeneratorOptions) { + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + await this.navigation.saveOptions(activeAccount.id, options); + if (options.type === "password") { + await this.passwords.saveOptions(activeAccount.id, options); + } else { + await this.passphrases.saveOptions(activeAccount.id, options); + } + } + + getHistory: () => Promise; + addHistory: (password: string) => Promise; + clear: (userId?: string) => Promise; +} diff --git a/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts b/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts new file mode 100644 index 0000000000..41d9c78dd2 --- /dev/null +++ b/libs/common/src/tools/generator/legacy-username-generation.service.spec.ts @@ -0,0 +1,748 @@ +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { mockAccountServiceWith } from "../../../spec"; +import { UserId } from "../../types/guid"; + +import { GeneratorNavigationService, GeneratorService } from "./abstractions"; +import { DefaultPolicyEvaluator } from "./default-policy-evaluator"; +import { LegacyUsernameGenerationService } from "./legacy-username-generation.service"; +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./navigation/generator-navigation"; +import { GeneratorNavigationEvaluator } from "./navigation/generator-navigation-evaluator"; +import { GeneratorNavigationPolicy } from "./navigation/generator-navigation-policy"; +import { NoPolicy } from "./no-policy"; +import { UsernameGeneratorOptions } from "./username"; +import { + CatchallGenerationOptions, + DefaultCatchallOptions, +} from "./username/catchall-generator-options"; +import { + DefaultEffUsernameOptions, + EffUsernameGenerationOptions, +} from "./username/eff-username-generator-options"; +import { DefaultAddyIoOptions } from "./username/forwarders/addy-io"; +import { DefaultDuckDuckGoOptions } from "./username/forwarders/duck-duck-go"; +import { DefaultFastmailOptions } from "./username/forwarders/fastmail"; +import { DefaultFirefoxRelayOptions } from "./username/forwarders/firefox-relay"; +import { DefaultForwardEmailOptions } from "./username/forwarders/forward-email"; +import { DefaultSimpleLoginOptions } from "./username/forwarders/simple-login"; +import { Forwarders } from "./username/options/constants"; +import { + ApiOptions, + EmailDomainOptions, + EmailPrefixOptions, + SelfHostedApiOptions, +} from "./username/options/forwarder-options"; +import { + DefaultSubaddressOptions, + SubaddressGenerationOptions, +} from "./username/subaddress-generator-options"; + +const SomeUser = "userId" as UserId; + +function createGenerator(options: Options, defaults: Options) { + let savedOptions = options; + const generator = mock>({ + evaluator$(id: UserId) { + const evaluator = new DefaultPolicyEvaluator(); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(defaults); + }, + saveOptions: jest.fn((userId, options) => { + savedOptions = options; + return Promise.resolve(); + }), + }); + + return generator; +} + +function createNavigationGenerator( + options: GeneratorNavigation = {}, + policy: GeneratorNavigationPolicy = {}, +) { + let savedOptions = options; + const generator = mock({ + evaluator$(id: UserId) { + const evaluator = new GeneratorNavigationEvaluator(policy); + return of(evaluator); + }, + options$(id: UserId) { + return of(savedOptions); + }, + defaults$(id: UserId) { + return of(DefaultGeneratorNavigation); + }, + saveOptions: jest.fn((userId, options) => { + savedOptions = options; + return Promise.resolve(); + }), + }); + + return generator; +} + +describe("LegacyUsernameGenerationService", () => { + // NOTE: in all tests, `null` constructor arguments are not used by the test. + // They're set to `null` to avoid setting up unnecessary mocks. + describe("generateUserName", () => { + it("should generate a catchall username", async () => { + const options = { type: "catchall" } as UsernameGeneratorOptions; + const catchall = createGenerator(null, null); + catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + catchall, + null, + null, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(catchall.generate).toHaveBeenCalledWith(options); + expect(result).toBe("catchall@example.com"); + }); + + it("should generate an EFF word username", async () => { + const options = { type: "word" } as UsernameGeneratorOptions; + const effWord = createGenerator(null, null); + effWord.generate.mockReturnValue(Promise.resolve("eff word")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + effWord, + null, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(effWord.generate).toHaveBeenCalledWith(options); + expect(result).toBe("eff word"); + }); + + it("should generate a subaddress username", async () => { + const options = { type: "subaddress" } as UsernameGeneratorOptions; + const subaddress = createGenerator(null, null); + subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + subaddress, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(subaddress.generate).toHaveBeenCalledWith(options); + expect(result).toBe("subaddress@example.com"); + }); + + it("should generate a forwarder username", async () => { + // set up an arbitrary forwarder for the username test; all forwarders tested in their own tests + const options = { + type: "forwarded", + forwardedService: Forwarders.AddyIo.id, + } as UsernameGeneratorOptions; + const addyIo = createGenerator(null, null); + addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + addyIo, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateUsername(options); + + expect(addyIo.generate).toHaveBeenCalledWith({}); + expect(result).toBe("addyio@example.com"); + }); + }); + + describe("generateCatchall", () => { + it("should generate a catchall username", async () => { + const options = { type: "catchall" } as UsernameGeneratorOptions; + const catchall = createGenerator(null, null); + catchall.generate.mockReturnValue(Promise.resolve("catchall@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + catchall, + null, + null, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateCatchall(options); + + expect(catchall.generate).toHaveBeenCalledWith(options); + expect(result).toBe("catchall@example.com"); + }); + }); + + describe("generateSubaddress", () => { + it("should generate a subaddress username", async () => { + const options = { type: "subaddress" } as UsernameGeneratorOptions; + const subaddress = createGenerator(null, null); + subaddress.generate.mockReturnValue(Promise.resolve("subaddress@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + subaddress, + null, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateSubaddress(options); + + expect(subaddress.generate).toHaveBeenCalledWith(options); + expect(result).toBe("subaddress@example.com"); + }); + }); + + describe("generateForwarded", () => { + it("should generate a AddyIo username", async () => { + const options = { + forwardedService: Forwarders.AddyIo.id, + forwardedAnonAddyApiToken: "token", + forwardedAnonAddyBaseUrl: "https://example.com", + forwardedAnonAddyDomain: "example.com", + website: "example.com", + } as UsernameGeneratorOptions; + const addyIo = createGenerator(null, null); + addyIo.generate.mockReturnValue(Promise.resolve("addyio@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + addyIo, + null, + null, + null, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(addyIo.generate).toHaveBeenCalledWith({ + token: "token", + baseUrl: "https://example.com", + domain: "example.com", + website: "example.com", + }); + expect(result).toBe("addyio@example.com"); + }); + + it("should generate a DuckDuckGo username", async () => { + const options = { + forwardedService: Forwarders.DuckDuckGo.id, + forwardedDuckDuckGoToken: "token", + website: "example.com", + } as UsernameGeneratorOptions; + const duckDuckGo = createGenerator(null, null); + duckDuckGo.generate.mockReturnValue(Promise.resolve("ddg@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + duckDuckGo, + null, + null, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(duckDuckGo.generate).toHaveBeenCalledWith({ + token: "token", + website: "example.com", + }); + expect(result).toBe("ddg@example.com"); + }); + + it("should generate a Fastmail username", async () => { + const options = { + forwardedService: Forwarders.Fastmail.id, + forwardedFastmailApiToken: "token", + website: "example.com", + } as UsernameGeneratorOptions; + const fastmail = createGenerator(null, null); + fastmail.generate.mockReturnValue(Promise.resolve("fastmail@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + fastmail, + null, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(fastmail.generate).toHaveBeenCalledWith({ + token: "token", + website: "example.com", + }); + expect(result).toBe("fastmail@example.com"); + }); + + it("should generate a FirefoxRelay username", async () => { + const options = { + forwardedService: Forwarders.FirefoxRelay.id, + forwardedFirefoxApiToken: "token", + website: "example.com", + } as UsernameGeneratorOptions; + const firefoxRelay = createGenerator(null, null); + firefoxRelay.generate.mockReturnValue(Promise.resolve("firefoxrelay@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + null, + firefoxRelay, + null, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(firefoxRelay.generate).toHaveBeenCalledWith({ + token: "token", + website: "example.com", + }); + expect(result).toBe("firefoxrelay@example.com"); + }); + + it("should generate a ForwardEmail username", async () => { + const options = { + forwardedService: Forwarders.ForwardEmail.id, + forwardedForwardEmailApiToken: "token", + forwardedForwardEmailDomain: "example.com", + website: "example.com", + } as UsernameGeneratorOptions; + const forwardEmail = createGenerator(null, null); + forwardEmail.generate.mockReturnValue(Promise.resolve("forwardemail@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + null, + null, + forwardEmail, + null, + ); + + const result = await generator.generateForwarded(options); + + expect(forwardEmail.generate).toHaveBeenCalledWith({ + token: "token", + domain: "example.com", + website: "example.com", + }); + expect(result).toBe("forwardemail@example.com"); + }); + + it("should generate a SimpleLogin username", async () => { + const options = { + forwardedService: Forwarders.SimpleLogin.id, + forwardedSimpleLoginApiKey: "token", + forwardedSimpleLoginBaseUrl: "https://example.com", + website: "example.com", + } as UsernameGeneratorOptions; + const simpleLogin = createGenerator(null, null); + simpleLogin.generate.mockReturnValue(Promise.resolve("simplelogin@example.com")); + const generator = new LegacyUsernameGenerationService( + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + simpleLogin, + ); + + const result = await generator.generateForwarded(options); + + expect(simpleLogin.generate).toHaveBeenCalledWith({ + token: "token", + baseUrl: "https://example.com", + website: "example.com", + }); + expect(result).toBe("simplelogin@example.com"); + }); + }); + + describe("getOptions", () => { + it("combines options from its inner generators", async () => { + const account = mockAccountServiceWith(SomeUser); + + const navigation = createNavigationGenerator({ + type: "username", + username: "catchall", + forwarder: Forwarders.AddyIo.id, + }); + + const catchall = createGenerator( + { + catchallDomain: "example.com", + catchallType: "random", + website: null, + }, + null, + ); + + const effUsername = createGenerator( + { + wordCapitalize: true, + wordIncludeNumber: false, + website: null, + }, + null, + ); + + const subaddress = createGenerator( + { + subaddressType: "random", + subaddressEmail: "foo@example.com", + website: null, + }, + null, + ); + + const addyIo = createGenerator( + { + token: "addyIoToken", + domain: "addyio.example.com", + baseUrl: "https://addyio.api.example.com", + website: null, + }, + null, + ); + + const duckDuckGo = createGenerator( + { + token: "ddgToken", + website: null, + }, + null, + ); + + const fastmail = createGenerator( + { + token: "fastmailToken", + domain: "fastmail.example.com", + prefix: "foo", + website: null, + }, + null, + ); + + const firefoxRelay = createGenerator( + { + token: "firefoxToken", + website: null, + }, + null, + ); + + const forwardEmail = createGenerator( + { + token: "forwardEmailToken", + domain: "example.com", + website: null, + }, + null, + ); + + const simpleLogin = createGenerator( + { + token: "simpleLoginToken", + baseUrl: "https://simplelogin.api.example.com", + website: null, + }, + null, + ); + + const generator = new LegacyUsernameGenerationService( + account, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); + + const result = await generator.getOptions(); + + expect(result).toEqual({ + type: "catchall", + wordCapitalize: true, + wordIncludeNumber: false, + subaddressType: "random", + subaddressEmail: "foo@example.com", + catchallType: "random", + catchallDomain: "example.com", + forwardedService: Forwarders.AddyIo.id, + forwardedAnonAddyApiToken: "addyIoToken", + forwardedAnonAddyDomain: "addyio.example.com", + forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", + forwardedDuckDuckGoToken: "ddgToken", + forwardedFirefoxApiToken: "firefoxToken", + forwardedFastmailApiToken: "fastmailToken", + forwardedForwardEmailApiToken: "forwardEmailToken", + forwardedForwardEmailDomain: "example.com", + forwardedSimpleLoginApiKey: "simpleLoginToken", + forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com", + }); + }); + + it("sets default options when an inner service lacks a value", async () => { + const account = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator(null); + const catchall = createGenerator(null, DefaultCatchallOptions); + const effUsername = createGenerator( + null, + DefaultEffUsernameOptions, + ); + const subaddress = createGenerator( + null, + DefaultSubaddressOptions, + ); + const addyIo = createGenerator( + null, + DefaultAddyIoOptions, + ); + const duckDuckGo = createGenerator(null, DefaultDuckDuckGoOptions); + const fastmail = createGenerator( + null, + DefaultFastmailOptions, + ); + const firefoxRelay = createGenerator(null, DefaultFirefoxRelayOptions); + const forwardEmail = createGenerator( + null, + DefaultForwardEmailOptions, + ); + const simpleLogin = createGenerator(null, DefaultSimpleLoginOptions); + + const generator = new LegacyUsernameGenerationService( + account, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); + + const result = await generator.getOptions(); + + expect(result).toEqual({ + type: DefaultGeneratorNavigation.username, + catchallType: DefaultCatchallOptions.catchallType, + catchallDomain: DefaultCatchallOptions.catchallDomain, + wordCapitalize: DefaultEffUsernameOptions.wordCapitalize, + wordIncludeNumber: DefaultEffUsernameOptions.wordIncludeNumber, + subaddressType: DefaultSubaddressOptions.subaddressType, + subaddressEmail: DefaultSubaddressOptions.subaddressEmail, + forwardedService: DefaultGeneratorNavigation.forwarder, + forwardedAnonAddyApiToken: DefaultAddyIoOptions.token, + forwardedAnonAddyDomain: DefaultAddyIoOptions.domain, + forwardedAnonAddyBaseUrl: DefaultAddyIoOptions.baseUrl, + forwardedDuckDuckGoToken: DefaultDuckDuckGoOptions.token, + forwardedFastmailApiToken: DefaultFastmailOptions.token, + forwardedFirefoxApiToken: DefaultFirefoxRelayOptions.token, + forwardedForwardEmailApiToken: DefaultForwardEmailOptions.token, + forwardedForwardEmailDomain: DefaultForwardEmailOptions.domain, + forwardedSimpleLoginApiKey: DefaultSimpleLoginOptions.token, + forwardedSimpleLoginBaseUrl: DefaultSimpleLoginOptions.baseUrl, + }); + }); + }); + + describe("saveOptions", () => { + it("saves option sets to its inner generators", async () => { + const account = mockAccountServiceWith(SomeUser); + const navigation = createNavigationGenerator({ type: "password" }); + const catchall = createGenerator(null, null); + const effUsername = createGenerator(null, null); + const subaddress = createGenerator(null, null); + const addyIo = createGenerator(null, null); + const duckDuckGo = createGenerator(null, null); + const fastmail = createGenerator(null, null); + const firefoxRelay = createGenerator(null, null); + const forwardEmail = createGenerator(null, null); + const simpleLogin = createGenerator(null, null); + + const generator = new LegacyUsernameGenerationService( + account, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); + + await generator.saveOptions({ + type: "catchall", + wordCapitalize: true, + wordIncludeNumber: false, + subaddressType: "random", + subaddressEmail: "foo@example.com", + catchallType: "random", + catchallDomain: "example.com", + forwardedService: Forwarders.AddyIo.id, + forwardedAnonAddyApiToken: "addyIoToken", + forwardedAnonAddyDomain: "addyio.example.com", + forwardedAnonAddyBaseUrl: "https://addyio.api.example.com", + forwardedDuckDuckGoToken: "ddgToken", + forwardedFirefoxApiToken: "firefoxToken", + forwardedFastmailApiToken: "fastmailToken", + forwardedForwardEmailApiToken: "forwardEmailToken", + forwardedForwardEmailDomain: "example.com", + forwardedSimpleLoginApiKey: "simpleLoginToken", + forwardedSimpleLoginBaseUrl: "https://simplelogin.api.example.com", + website: null, + }); + + expect(navigation.saveOptions).toHaveBeenCalledWith(SomeUser, { + type: "password", + username: "catchall", + forwarder: Forwarders.AddyIo.id, + }); + + expect(catchall.saveOptions).toHaveBeenCalledWith(SomeUser, { + catchallDomain: "example.com", + catchallType: "random", + website: null, + }); + + expect(effUsername.saveOptions).toHaveBeenCalledWith(SomeUser, { + wordCapitalize: true, + wordIncludeNumber: false, + website: null, + }); + + expect(subaddress.saveOptions).toHaveBeenCalledWith(SomeUser, { + subaddressType: "random", + subaddressEmail: "foo@example.com", + website: null, + }); + + expect(addyIo.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "addyIoToken", + domain: "addyio.example.com", + baseUrl: "https://addyio.api.example.com", + website: null, + }); + + expect(duckDuckGo.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "ddgToken", + website: null, + }); + + expect(fastmail.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "fastmailToken", + website: null, + }); + + expect(firefoxRelay.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "firefoxToken", + website: null, + }); + + expect(forwardEmail.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "forwardEmailToken", + domain: "example.com", + website: null, + }); + + expect(simpleLogin.saveOptions).toHaveBeenCalledWith(SomeUser, { + token: "simpleLoginToken", + baseUrl: "https://simplelogin.api.example.com", + website: null, + }); + }); + }); +}); diff --git a/libs/common/src/tools/generator/legacy-username-generation.service.ts b/libs/common/src/tools/generator/legacy-username-generation.service.ts new file mode 100644 index 0000000000..7611a86c27 --- /dev/null +++ b/libs/common/src/tools/generator/legacy-username-generation.service.ts @@ -0,0 +1,383 @@ +import { zip, firstValueFrom, map, concatMap } from "rxjs"; + +import { ApiService } from "../../abstractions/api.service"; +import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { CryptoService } from "../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { I18nService } from "../../platform/abstractions/i18n.service"; +import { StateProvider } from "../../platform/state"; + +import { GeneratorService, GeneratorNavigationService } from "./abstractions"; +import { UsernameGenerationServiceAbstraction } from "./abstractions/username-generation.service.abstraction"; +import { DefaultGeneratorService } from "./default-generator.service"; +import { DefaultGeneratorNavigationService } from "./navigation/default-generator-navigation.service"; +import { GeneratorNavigation } from "./navigation/generator-navigation"; +import { NoPolicy } from "./no-policy"; +import { + CatchallGeneratorStrategy, + SubaddressGeneratorStrategy, + EffUsernameGeneratorStrategy, +} from "./username"; +import { CatchallGenerationOptions } from "./username/catchall-generator-options"; +import { EffUsernameGenerationOptions } from "./username/eff-username-generator-options"; +import { AddyIoForwarder } from "./username/forwarders/addy-io"; +import { DuckDuckGoForwarder } from "./username/forwarders/duck-duck-go"; +import { FastmailForwarder } from "./username/forwarders/fastmail"; +import { FirefoxRelayForwarder } from "./username/forwarders/firefox-relay"; +import { ForwardEmailForwarder } from "./username/forwarders/forward-email"; +import { SimpleLoginForwarder } from "./username/forwarders/simple-login"; +import { Forwarders } from "./username/options/constants"; +import { + ApiOptions, + EmailDomainOptions, + EmailPrefixOptions, + RequestOptions, + SelfHostedApiOptions, +} from "./username/options/forwarder-options"; +import { SubaddressGenerationOptions } from "./username/subaddress-generator-options"; +import { UsernameGeneratorOptions } from "./username/username-generation-options"; +import { UsernameGenerationService } from "./username/username-generation.service"; + +type MappedOptions = { + generator: GeneratorNavigation; + algorithms: { + catchall: CatchallGenerationOptions; + effUsername: EffUsernameGenerationOptions; + subaddress: SubaddressGenerationOptions; + }; + forwarders: { + addyIo: SelfHostedApiOptions & EmailDomainOptions & RequestOptions; + duckDuckGo: ApiOptions & RequestOptions; + fastmail: ApiOptions & EmailPrefixOptions & RequestOptions; + firefoxRelay: ApiOptions & RequestOptions; + forwardEmail: ApiOptions & EmailDomainOptions & RequestOptions; + simpleLogin: SelfHostedApiOptions & RequestOptions; + }; +}; + +export function legacyPasswordGenerationServiceFactory( + apiService: ApiService, + i18nService: I18nService, + cryptoService: CryptoService, + encryptService: EncryptService, + policyService: PolicyService, + accountService: AccountService, + stateProvider: StateProvider, +): UsernameGenerationServiceAbstraction { + // FIXME: Once the username generation service is replaced with this service + // in the clients, factor out the deprecated service in its entirety. + const deprecatedService = new UsernameGenerationService(cryptoService, null, null); + + const effUsername = new DefaultGeneratorService( + new EffUsernameGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const subaddress = new DefaultGeneratorService( + new SubaddressGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const catchall = new DefaultGeneratorService( + new CatchallGeneratorStrategy(deprecatedService, stateProvider), + policyService, + ); + + const addyIo = new DefaultGeneratorService( + new AddyIoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const duckDuckGo = new DefaultGeneratorService( + new DuckDuckGoForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const fastmail = new DefaultGeneratorService( + new FastmailForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const firefoxRelay = new DefaultGeneratorService( + new FirefoxRelayForwarder( + apiService, + i18nService, + encryptService, + cryptoService, + stateProvider, + ), + policyService, + ); + + const forwardEmail = new DefaultGeneratorService( + new ForwardEmailForwarder( + apiService, + i18nService, + encryptService, + cryptoService, + stateProvider, + ), + policyService, + ); + + const simpleLogin = new DefaultGeneratorService( + new SimpleLoginForwarder(apiService, i18nService, encryptService, cryptoService, stateProvider), + policyService, + ); + + const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService); + + return new LegacyUsernameGenerationService( + accountService, + navigation, + catchall, + effUsername, + subaddress, + addyIo, + duckDuckGo, + fastmail, + firefoxRelay, + forwardEmail, + simpleLogin, + ); +} + +/** Adapts the generator 2.0 design to 1.0 angular services. */ +export class LegacyUsernameGenerationService implements UsernameGenerationServiceAbstraction { + constructor( + private readonly accountService: AccountService, + private readonly navigation: GeneratorNavigationService, + private readonly catchall: GeneratorService, + private readonly effUsername: GeneratorService, + private readonly subaddress: GeneratorService, + private readonly addyIo: GeneratorService, + private readonly duckDuckGo: GeneratorService, + private readonly fastmail: GeneratorService, + private readonly firefoxRelay: GeneratorService, + private readonly forwardEmail: GeneratorService, + private readonly simpleLogin: GeneratorService, + ) {} + + generateUsername(options: UsernameGeneratorOptions) { + if (options.type === "catchall") { + return this.generateCatchall(options); + } else if (options.type === "subaddress") { + return this.generateSubaddress(options); + } else if (options.type === "forwarded") { + return this.generateForwarded(options); + } else { + return this.generateWord(options); + } + } + + generateWord(options: UsernameGeneratorOptions) { + return this.effUsername.generate(options); + } + + generateSubaddress(options: UsernameGeneratorOptions) { + return this.subaddress.generate(options); + } + + generateCatchall(options: UsernameGeneratorOptions) { + return this.catchall.generate(options); + } + + generateForwarded(options: UsernameGeneratorOptions) { + if (!options.forwardedService) { + return null; + } + + const stored = this.toStoredOptions(options); + switch (options.forwardedService) { + case Forwarders.AddyIo.id: + return this.addyIo.generate(stored.forwarders.addyIo); + case Forwarders.DuckDuckGo.id: + return this.duckDuckGo.generate(stored.forwarders.duckDuckGo); + case Forwarders.Fastmail.id: + return this.fastmail.generate(stored.forwarders.fastmail); + case Forwarders.FirefoxRelay.id: + return this.firefoxRelay.generate(stored.forwarders.firefoxRelay); + case Forwarders.ForwardEmail.id: + return this.forwardEmail.generate(stored.forwarders.forwardEmail); + case Forwarders.SimpleLogin.id: + return this.simpleLogin.generate(stored.forwarders.simpleLogin); + } + } + + getOptions() { + const options$ = this.accountService.activeAccount$.pipe( + concatMap((account) => + zip( + this.navigation.options$(account.id), + this.navigation.defaults$(account.id), + this.catchall.options$(account.id), + this.catchall.defaults$(account.id), + this.effUsername.options$(account.id), + this.effUsername.defaults$(account.id), + this.subaddress.options$(account.id), + this.subaddress.defaults$(account.id), + this.addyIo.options$(account.id), + this.addyIo.defaults$(account.id), + this.duckDuckGo.options$(account.id), + this.duckDuckGo.defaults$(account.id), + this.fastmail.options$(account.id), + this.fastmail.defaults$(account.id), + this.firefoxRelay.options$(account.id), + this.firefoxRelay.defaults$(account.id), + this.forwardEmail.options$(account.id), + this.forwardEmail.defaults$(account.id), + this.simpleLogin.options$(account.id), + this.simpleLogin.defaults$(account.id), + ), + ), + map( + ([ + generatorOptions, + generatorDefaults, + catchallOptions, + catchallDefaults, + effUsernameOptions, + effUsernameDefaults, + subaddressOptions, + subaddressDefaults, + addyIoOptions, + addyIoDefaults, + duckDuckGoOptions, + duckDuckGoDefaults, + fastmailOptions, + fastmailDefaults, + firefoxRelayOptions, + firefoxRelayDefaults, + forwardEmailOptions, + forwardEmailDefaults, + simpleLoginOptions, + simpleLoginDefaults, + ]) => + this.toUsernameOptions({ + generator: generatorOptions ?? generatorDefaults, + algorithms: { + catchall: catchallOptions ?? catchallDefaults, + effUsername: effUsernameOptions ?? effUsernameDefaults, + subaddress: subaddressOptions ?? subaddressDefaults, + }, + forwarders: { + addyIo: addyIoOptions ?? addyIoDefaults, + duckDuckGo: duckDuckGoOptions ?? duckDuckGoDefaults, + fastmail: fastmailOptions ?? fastmailDefaults, + firefoxRelay: firefoxRelayOptions ?? firefoxRelayDefaults, + forwardEmail: forwardEmailOptions ?? forwardEmailDefaults, + simpleLogin: simpleLoginOptions ?? simpleLoginDefaults, + }, + }), + ), + ); + + return firstValueFrom(options$); + } + + async saveOptions(options: UsernameGeneratorOptions) { + const stored = this.toStoredOptions(options); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a.id))); + + // generator settings needs to preserve whether password or passphrase is selected, + // so `navigationOptions` is mutated. + let navigationOptions = await firstValueFrom(this.navigation.options$(userId)); + navigationOptions = Object.assign(navigationOptions, stored.generator); + await this.navigation.saveOptions(userId, navigationOptions); + + // overwrite all other settings with latest values + await Promise.all([ + this.catchall.saveOptions(userId, stored.algorithms.catchall), + this.effUsername.saveOptions(userId, stored.algorithms.effUsername), + this.subaddress.saveOptions(userId, stored.algorithms.subaddress), + this.addyIo.saveOptions(userId, stored.forwarders.addyIo), + this.duckDuckGo.saveOptions(userId, stored.forwarders.duckDuckGo), + this.fastmail.saveOptions(userId, stored.forwarders.fastmail), + this.firefoxRelay.saveOptions(userId, stored.forwarders.firefoxRelay), + this.forwardEmail.saveOptions(userId, stored.forwarders.forwardEmail), + this.simpleLogin.saveOptions(userId, stored.forwarders.simpleLogin), + ]); + } + + private toStoredOptions(options: UsernameGeneratorOptions) { + const forwarders = { + addyIo: { + baseUrl: options.forwardedAnonAddyBaseUrl, + token: options.forwardedAnonAddyApiToken, + domain: options.forwardedAnonAddyDomain, + website: options.website, + }, + duckDuckGo: { + token: options.forwardedDuckDuckGoToken, + website: options.website, + }, + fastmail: { + token: options.forwardedFastmailApiToken, + website: options.website, + }, + firefoxRelay: { + token: options.forwardedFirefoxApiToken, + website: options.website, + }, + forwardEmail: { + token: options.forwardedForwardEmailApiToken, + domain: options.forwardedForwardEmailDomain, + website: options.website, + }, + simpleLogin: { + token: options.forwardedSimpleLoginApiKey, + baseUrl: options.forwardedSimpleLoginBaseUrl, + website: options.website, + }, + }; + + const generator = { + username: options.type, + forwarder: options.forwardedService, + }; + + const algorithms = { + effUsername: { + wordCapitalize: options.wordCapitalize, + wordIncludeNumber: options.wordIncludeNumber, + website: options.website, + }, + subaddress: { + subaddressType: options.subaddressType, + subaddressEmail: options.subaddressEmail, + website: options.website, + }, + catchall: { + catchallType: options.catchallType, + catchallDomain: options.catchallDomain, + website: options.website, + }, + }; + + return { generator, algorithms, forwarders } as MappedOptions; + } + + private toUsernameOptions(options: MappedOptions) { + return { + type: options.generator.username, + wordCapitalize: options.algorithms.effUsername.wordCapitalize, + wordIncludeNumber: options.algorithms.effUsername.wordIncludeNumber, + subaddressType: options.algorithms.subaddress.subaddressType, + subaddressEmail: options.algorithms.subaddress.subaddressEmail, + catchallType: options.algorithms.catchall.catchallType, + catchallDomain: options.algorithms.catchall.catchallDomain, + forwardedService: options.generator.forwarder, + forwardedAnonAddyApiToken: options.forwarders.addyIo.token, + forwardedAnonAddyDomain: options.forwarders.addyIo.domain, + forwardedAnonAddyBaseUrl: options.forwarders.addyIo.baseUrl, + forwardedDuckDuckGoToken: options.forwarders.duckDuckGo.token, + forwardedFirefoxApiToken: options.forwarders.firefoxRelay.token, + forwardedFastmailApiToken: options.forwarders.fastmail.token, + forwardedForwardEmailApiToken: options.forwarders.forwardEmail.token, + forwardedForwardEmailDomain: options.forwarders.forwardEmail.domain, + forwardedSimpleLoginApiKey: options.forwarders.simpleLogin.token, + forwardedSimpleLoginBaseUrl: options.forwarders.simpleLogin.baseUrl, + } as UsernameGeneratorOptions; + } +} diff --git a/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts b/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts new file mode 100644 index 0000000000..6853542bb7 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/default-generator-nativation.service.spec.ts @@ -0,0 +1,100 @@ +/** + * include structuredClone in test environment. + * @jest-environment ../../../../shared/test.environment.ts + */ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { UserId } from "../../../types/guid"; +import { GENERATOR_SETTINGS } from "../key-definitions"; + +import { + GeneratorNavigationEvaluator, + DefaultGeneratorNavigationService, + DefaultGeneratorNavigation, +} from "./"; + +const SomeUser = "some user" as UserId; + +describe("DefaultGeneratorNavigationService", () => { + describe("options$", () => { + it("emits options", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const settings = { type: "password" as const }; + await stateProvider.setUserState(GENERATOR_SETTINGS, settings, SomeUser); + const navigation = new DefaultGeneratorNavigationService(stateProvider, null); + + const result = await firstValueFrom(navigation.options$(SomeUser)); + + expect(result).toEqual(settings); + }); + }); + + describe("defaults$", () => { + it("emits default options", async () => { + const navigation = new DefaultGeneratorNavigationService(null, null); + + const result = await firstValueFrom(navigation.defaults$(SomeUser)); + + expect(result).toEqual(DefaultGeneratorNavigation); + }); + }); + + describe("evaluator$", () => { + it("emits a GeneratorNavigationEvaluator", async () => { + const policyService = mock({ + getAll$() { + return of([]); + }, + }); + const navigation = new DefaultGeneratorNavigationService(null, policyService); + + const result = await firstValueFrom(navigation.evaluator$(SomeUser)); + + expect(result).toBeInstanceOf(GeneratorNavigationEvaluator); + }); + }); + + describe("enforcePolicy", () => { + it("applies policy", async () => { + const policyService = mock({ + getAll$(_type: PolicyType, _user: UserId) { + return of([ + new Policy({ + id: "" as any, + organizationId: "" as any, + enabled: true, + type: PolicyType.PasswordGenerator, + data: { defaultType: "password" }, + }), + ]); + }, + }); + const navigation = new DefaultGeneratorNavigationService(null, policyService); + const options = {}; + + const result = await navigation.enforcePolicy(SomeUser, options); + + expect(result).toMatchObject({ type: "password" }); + }); + }); + + describe("saveOptions", () => { + it("updates options$", async () => { + const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); + const navigation = new DefaultGeneratorNavigationService(stateProvider, null); + const settings = { type: "password" as const }; + + await navigation.saveOptions(SomeUser, settings); + const result = await firstValueFrom(navigation.options$(SomeUser)); + + expect(result).toEqual(settings); + }); + }); +}); diff --git a/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts b/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts new file mode 100644 index 0000000000..3199efc8c3 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/default-generator-navigation.service.ts @@ -0,0 +1,71 @@ +import { BehaviorSubject, Observable, firstValueFrom, map } from "rxjs"; + +import { PolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "../../../admin-console/enums"; +import { StateProvider } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; +import { GeneratorNavigationService } from "../abstractions/generator-navigation.service.abstraction"; +import { GENERATOR_SETTINGS } from "../key-definitions"; +import { reduceCollection } from "../reduce-collection.operator"; + +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation"; +import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; +import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy"; + +export class DefaultGeneratorNavigationService implements GeneratorNavigationService { + /** instantiates the password generator strategy. + * @param stateProvider provides durable state + * @param policy provides the policy to enforce + */ + constructor( + private readonly stateProvider: StateProvider, + private readonly policy: PolicyService, + ) {} + + /** An observable monitoring the options saved to disk. + * The observable updates when the options are saved. + * @param userId: Identifies the user making the request + */ + options$(userId: UserId): Observable { + return this.stateProvider.getUserState$(GENERATOR_SETTINGS, userId); + } + + /** Gets the default options. */ + defaults$(userId: UserId): Observable { + return new BehaviorSubject({ ...DefaultGeneratorNavigation }); + } + + /** An observable monitoring the options used to enforce policy. + * The observable updates when the policy changes. + * @param userId: Identifies the user making the request + */ + evaluator$(userId: UserId) { + const evaluator$ = this.policy.getAll$(PolicyType.PasswordGenerator, userId).pipe( + reduceCollection(preferPassword, DisabledGeneratorNavigationPolicy), + map((policy) => new GeneratorNavigationEvaluator(policy)), + ); + + return evaluator$; + } + + /** Enforces the policy on the given options + * @param userId: Identifies the user making the request + * @param options the options to enforce the policy on + * @returns a new instance of the options with the policy enforced + */ + async enforcePolicy(userId: UserId, options: GeneratorNavigation) { + const evaluator = await firstValueFrom(this.evaluator$(userId)); + const applied = evaluator.applyPolicy(options); + const sanitized = evaluator.sanitize(applied); + return sanitized; + } + + /** Saves the navigation options to disk. + * @param userId: Identifies the user making the request + * @param options the options to save + * @returns a promise that resolves when the options are saved + */ + async saveOptions(userId: UserId, options: GeneratorNavigation): Promise { + await this.stateProvider.setUserState(GENERATOR_SETTINGS, options, userId); + } +} diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts new file mode 100644 index 0000000000..58560fb5a0 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.spec.ts @@ -0,0 +1,64 @@ +import { DefaultGeneratorNavigation } from "./generator-navigation"; +import { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; + +describe("GeneratorNavigationEvaluator", () => { + describe("policyInEffect", () => { + it.each([["passphrase"], ["password"]] as const)( + "returns true if the policy has a defaultType (= %p)", + (defaultType) => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType }); + + expect(evaluator.policyInEffect).toEqual(true); + }, + ); + + it.each([[undefined], [null], ["" as any]])( + "returns false if the policy has a falsy defaultType (= %p)", + (defaultType) => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType }); + + expect(evaluator.policyInEffect).toEqual(false); + }, + ); + }); + + describe("applyPolicy", () => { + it("returns the input options", () => { + const evaluator = new GeneratorNavigationEvaluator(null); + const options = { type: "password" as const }; + + const result = evaluator.applyPolicy(options); + + expect(result).toEqual(options); + }); + }); + + describe("sanitize", () => { + it.each([["passphrase"], ["password"]] as const)( + "defaults options to the policy's default type (= %p) when a policy is in effect", + (defaultType) => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType }); + + const result = evaluator.sanitize({}); + + expect(result).toEqual({ type: defaultType }); + }, + ); + + it("defaults options to the default generator navigation type when a policy is not in effect", () => { + const evaluator = new GeneratorNavigationEvaluator(null); + + const result = evaluator.sanitize({}); + + expect(result.type).toEqual(DefaultGeneratorNavigation.type); + }); + + it("retains the options type when it is set", () => { + const evaluator = new GeneratorNavigationEvaluator({ defaultType: "passphrase" }); + + const result = evaluator.sanitize({ type: "password" }); + + expect(result).toEqual({ type: "password" }); + }); + }); +}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts new file mode 100644 index 0000000000..e580f130b5 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-evaluator.ts @@ -0,0 +1,43 @@ +import { PolicyEvaluator } from "../abstractions"; + +import { DefaultGeneratorNavigation, GeneratorNavigation } from "./generator-navigation"; +import { GeneratorNavigationPolicy } from "./generator-navigation-policy"; + +/** Enforces policy for generator navigation options. + */ +export class GeneratorNavigationEvaluator + implements PolicyEvaluator +{ + /** Instantiates the evaluator. + * @param policy The policy applied by the evaluator. When this conflicts with + * the defaults, the policy takes precedence. + */ + constructor(readonly policy: GeneratorNavigationPolicy) {} + + /** {@link PolicyEvaluator.policyInEffect} */ + get policyInEffect(): boolean { + return this.policy?.defaultType ? true : false; + } + + /** Apply policy to the input options. + * @param options The options to build from. These options are not altered. + * @returns A new password generation request with policy applied. + */ + applyPolicy(options: GeneratorNavigation): GeneratorNavigation { + return options; + } + + /** Ensures internal options consistency. + * @param options The options to cascade. These options are not altered. + * @returns A passphrase generation request with cascade applied. + */ + sanitize(options: GeneratorNavigation): GeneratorNavigation { + const defaultType = this.policyInEffect + ? this.policy.defaultType + : DefaultGeneratorNavigation.type; + return { + ...options, + type: options.type ?? defaultType, + }; + } +} diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts b/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts new file mode 100644 index 0000000000..ed8fe731a7 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-policy.spec.ts @@ -0,0 +1,63 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { PolicyId } from "../../../types/guid"; + +import { DisabledGeneratorNavigationPolicy, preferPassword } from "./generator-navigation-policy"; + +function createPolicy( + data: any, + type: PolicyType = PolicyType.PasswordGenerator, + enabled: boolean = true, +) { + return new Policy({ + id: "id" as PolicyId, + organizationId: "organizationId", + data, + enabled, + type, + }); +} + +describe("leastPrivilege", () => { + it("should return the accumulator when the policy type does not apply", () => { + const policy = createPolicy({}, PolicyType.RequireSso); + + const result = preferPassword(DisabledGeneratorNavigationPolicy, policy); + + expect(result).toEqual(DisabledGeneratorNavigationPolicy); + }); + + it("should return the accumulator when the policy is not enabled", () => { + const policy = createPolicy({}, PolicyType.PasswordGenerator, false); + + const result = preferPassword(DisabledGeneratorNavigationPolicy, policy); + + expect(result).toEqual(DisabledGeneratorNavigationPolicy); + }); + + it("should take the %p from the policy", () => { + const policy = createPolicy({ defaultType: "passphrase" }); + + const result = preferPassword({ ...DisabledGeneratorNavigationPolicy }, policy); + + expect(result).toEqual({ defaultType: "passphrase" }); + }); + + it("should override passphrase with password", () => { + const policy = createPolicy({ defaultType: "password" }); + + const result = preferPassword({ defaultType: "passphrase" }, policy); + + expect(result).toEqual({ defaultType: "password" }); + }); + + it("should not override password", () => { + const policy = createPolicy({ defaultType: "passphrase" }); + + const result = preferPassword({ defaultType: "password" }, policy); + + expect(result).toEqual({ defaultType: "password" }); + }); +}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts b/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts new file mode 100644 index 0000000000..25c2a73337 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation-policy.ts @@ -0,0 +1,39 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { GeneratorType } from "../generator-type"; + +/** Policy settings affecting password generator navigation */ +export type GeneratorNavigationPolicy = { + /** The type of generator that should be shown by default when opening + * the password generator. + */ + defaultType?: GeneratorType; +}; + +/** Reduces a policy into an accumulator by preferring the password generator + * type to other generator types. + * @param acc the accumulator + * @param policy the policy to reduce + * @returns the resulting `GeneratorNavigationPolicy` + */ +export function preferPassword( + acc: GeneratorNavigationPolicy, + policy: Policy, +): GeneratorNavigationPolicy { + const isEnabled = policy.type === PolicyType.PasswordGenerator && policy.enabled; + if (!isEnabled) { + return acc; + } + + const isOverridable = acc.defaultType !== "password" && policy.data.defaultType; + const result = isOverridable ? { ...acc, defaultType: policy.data.defaultType } : acc; + + return result; +} + +/** The default options for password generation policy. */ +export const DisabledGeneratorNavigationPolicy: GeneratorNavigationPolicy = Object.freeze({ + defaultType: undefined, +}); diff --git a/libs/common/src/tools/generator/navigation/generator-navigation.ts b/libs/common/src/tools/generator/navigation/generator-navigation.ts new file mode 100644 index 0000000000..6a07385286 --- /dev/null +++ b/libs/common/src/tools/generator/navigation/generator-navigation.ts @@ -0,0 +1,26 @@ +import { GeneratorType } from "../generator-type"; +import { ForwarderId } from "../username/options"; +import { UsernameGeneratorType } from "../username/options/generator-options"; + +/** Stores credential generator UI state. */ + +export type GeneratorNavigation = { + /** The kind of credential being generated. + * @remarks The legacy generator only supports "password" and "passphrase". + * The componentized generator supports all values. + */ + type?: GeneratorType; + + /** When `type === "username"`, this stores the username algorithm. */ + username?: UsernameGeneratorType; + + /** When `username === "forwarded"`, this stores the forwarder implementation. */ + forwarder?: ForwarderId | ""; +}; +/** The default options for password generation. */ + +export const DefaultGeneratorNavigation: Partial = Object.freeze({ + type: "password", + username: "word", + forwarder: "", +}); diff --git a/libs/common/src/tools/generator/navigation/index.ts b/libs/common/src/tools/generator/navigation/index.ts new file mode 100644 index 0000000000..86194f471a --- /dev/null +++ b/libs/common/src/tools/generator/navigation/index.ts @@ -0,0 +1,3 @@ +export { GeneratorNavigationEvaluator } from "./generator-navigation-evaluator"; +export { DefaultGeneratorNavigationService } from "./default-generator-navigation.service"; +export { GeneratorNavigation, DefaultGeneratorNavigation } from "./generator-navigation"; diff --git a/libs/common/src/tools/generator/passphrase/index.ts b/libs/common/src/tools/generator/passphrase/index.ts index 175f15663e..3bbe925301 100644 --- a/libs/common/src/tools/generator/passphrase/index.ts +++ b/libs/common/src/tools/generator/passphrase/index.ts @@ -2,4 +2,7 @@ export { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; export { PassphraseGeneratorPolicy } from "./passphrase-generator-policy"; export { PassphraseGeneratorStrategy } from "./passphrase-generator-strategy"; -export { DefaultPassphraseGenerationOptions } from "./passphrase-generation-options"; +export { + DefaultPassphraseGenerationOptions, + PassphraseGenerationOptions, +} from "./passphrase-generation-options"; diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts new file mode 100644 index 0000000000..991b2ae302 --- /dev/null +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.spec.ts @@ -0,0 +1,51 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { PolicyId } from "../../../types/guid"; + +import { DisabledPassphraseGeneratorPolicy, leastPrivilege } from "./passphrase-generator-policy"; + +function createPolicy( + data: any, + type: PolicyType = PolicyType.PasswordGenerator, + enabled: boolean = true, +) { + return new Policy({ + id: "id" as PolicyId, + organizationId: "organizationId", + data, + enabled, + type, + }); +} + +describe("leastPrivilege", () => { + it("should return the accumulator when the policy type does not apply", () => { + const policy = createPolicy({}, PolicyType.RequireSso); + + const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPassphraseGeneratorPolicy); + }); + + it("should return the accumulator when the policy is not enabled", () => { + const policy = createPolicy({}, PolicyType.PasswordGenerator, false); + + const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPassphraseGeneratorPolicy); + }); + + it.each([ + ["minNumberWords", 10], + ["capitalize", true], + ["includeNumber", true], + ])("should take the %p from the policy", (input, value) => { + const policy = createPolicy({ [input]: value }); + + const result = leastPrivilege(DisabledPassphraseGeneratorPolicy, policy); + + expect(result).toEqual({ ...DisabledPassphraseGeneratorPolicy, [input]: value }); + }); +}); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts index ca54184d16..db616f16c0 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-policy.ts @@ -1,3 +1,8 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; + /** Policy options enforced during passphrase generation. */ export type PassphraseGeneratorPolicy = { minNumberWords: number; @@ -11,3 +16,24 @@ export const DisabledPassphraseGeneratorPolicy: PassphraseGeneratorPolicy = Obje capitalize: false, includeNumber: false, }); + +/** Reduces a policy into an accumulator by accepting the most restrictive + * values from each policy. + * @param acc the accumulator + * @param policy the policy to reduce + * @returns the most restrictive values between the policy and accumulator. + */ +export function leastPrivilege( + acc: PassphraseGeneratorPolicy, + policy: Policy, +): PassphraseGeneratorPolicy { + if (policy.type !== PolicyType.PasswordGenerator) { + return acc; + } + + return { + minNumberWords: Math.max(acc.minNumberWords, policy.data.minNumberWords ?? acc.minNumberWords), + capitalize: policy.data.capitalize || acc.capitalize, + includeNumber: policy.data.includeNumber || acc.includeNumber, + }; +} diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts index 031ea05f01..adcfc39527 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.spec.ts @@ -2,8 +2,8 @@ * include structuredClone in test environment. * @jest-environment ../../../../shared/test.environment.ts */ - import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -11,27 +11,22 @@ import { PolicyType } from "../../../admin-console/enums"; import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PASSPHRASE_SETTINGS } from "../key-definitions"; -import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction"; import { DisabledPassphraseGeneratorPolicy } from "./passphrase-generator-policy"; -import { PassphraseGeneratorOptionsEvaluator, PassphraseGeneratorStrategy } from "."; +import { + DefaultPassphraseGenerationOptions, + PassphraseGeneratorOptionsEvaluator, + PassphraseGeneratorStrategy, +} from "."; const SomeUser = "some user" as UserId; describe("Password generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new PassphraseGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.DisableSend, - }); - - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); - - it("should map to the policy evaluator", () => { + describe("toEvaluator()", () => { + it("should map to the policy evaluator", async () => { const strategy = new PassphraseGeneratorStrategy(null, null); const policy = mock({ type: PolicyType.PasswordGenerator, @@ -42,7 +37,8 @@ describe("Password generation strategy", () => { }, }); - const evaluator = strategy.evaluator(policy); + const evaluator$ = of([policy]).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); expect(evaluator.policy).toMatchObject({ @@ -52,13 +48,18 @@ describe("Password generation strategy", () => { }); }); - it("should map `null` to a default policy evaluator", () => { - const strategy = new PassphraseGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); + it.each([[[]], [null], [undefined]])( + "should map `%p` to a disabled password policy evaluator", + async (policies) => { + const strategy = new PassphraseGeneratorStrategy(null, null); - expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy); - }); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); + + expect(evaluator).toBeInstanceOf(PassphraseGeneratorOptionsEvaluator); + expect(evaluator.policy).toMatchObject(DisabledPassphraseGeneratorPolicy); + }, + ); }); describe("durableState", () => { @@ -73,6 +74,16 @@ describe("Password generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new PassphraseGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultPassphraseGenerationOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock(); diff --git a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts index d39f54b576..1a7c24082f 100644 --- a/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts +++ b/libs/common/src/tools/generator/passphrase/passphrase-generator-strategy.ts @@ -1,18 +1,22 @@ +import { BehaviorSubject, map, pipe } from "rxjs"; + import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PASSPHRASE_SETTINGS } from "../key-definitions"; -import { PasswordGenerationServiceAbstraction } from "../password/password-generation.service.abstraction"; +import { reduceCollection } from "../reduce-collection.operator"; -import { PassphraseGenerationOptions } from "./passphrase-generation-options"; +import { + PassphraseGenerationOptions, + DefaultPassphraseGenerationOptions, +} from "./passphrase-generation-options"; import { PassphraseGeneratorOptionsEvaluator } from "./passphrase-generator-options-evaluator"; import { DisabledPassphraseGeneratorPolicy, PassphraseGeneratorPolicy, + leastPrivilege, } from "./passphrase-generator-policy"; const ONE_MINUTE = 60 * 1000; @@ -23,6 +27,7 @@ export class PassphraseGeneratorStrategy { /** instantiates the password generator strategy. * @param legacy generates the passphrase + * @param stateProvider provides durable state */ constructor( private legacy: PasswordGenerationServiceAbstraction, @@ -34,31 +39,27 @@ export class PassphraseGeneratorStrategy return this.stateProvider.getUser(id, PASSPHRASE_SETTINGS); } + /** Gets the default options. */ + defaults$(_: UserId) { + return new BehaviorSubject({ ...DefaultPassphraseGenerationOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { return PolicyType.PasswordGenerator; } + /** {@link GeneratorStrategy.cache_ms} */ get cache_ms() { return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy): PassphraseGeneratorOptionsEvaluator { - if (!policy) { - return new PassphraseGeneratorOptionsEvaluator(DisabledPassphraseGeneratorPolicy); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new PassphraseGeneratorOptionsEvaluator({ - minNumberWords: policy.data.minNumberWords, - capitalize: policy.data.capitalize, - includeNumber: policy.data.includeNumber, - }); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe( + reduceCollection(leastPrivilege, DisabledPassphraseGeneratorPolicy), + map((policy) => new PassphraseGeneratorOptionsEvaluator(policy)), + ); } /** {@link GeneratorStrategy.generate} */ diff --git a/libs/common/src/tools/generator/password/index.ts b/libs/common/src/tools/generator/password/index.ts index 0fcbbf5616..e17ab8201c 100644 --- a/libs/common/src/tools/generator/password/index.ts +++ b/libs/common/src/tools/generator/password/index.ts @@ -6,6 +6,6 @@ export { PasswordGeneratorStrategy } from "./password-generator-strategy"; // legacy interfaces export { PasswordGeneratorOptions } from "./password-generator-options"; -export { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; +export { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; export { PasswordGenerationService } from "./password-generation.service"; export { GeneratedPasswordHistory } from "./generated-password-history"; diff --git a/libs/common/src/tools/generator/password/password-generation.service.ts b/libs/common/src/tools/generator/password/password-generation.service.ts index eb1f08d97e..fced2dfe43 100644 --- a/libs/common/src/tools/generator/password/password-generation.service.ts +++ b/libs/common/src/tools/generator/password/password-generation.service.ts @@ -5,10 +5,10 @@ import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { EFFLongWordList } from "../../../platform/misc/wordlist"; import { EncString } from "../../../platform/models/domain/enc-string"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PassphraseGeneratorOptionsEvaluator } from "../passphrase/passphrase-generator-options-evaluator"; import { GeneratedPasswordHistory } from "./generated-password-history"; -import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; import { PasswordGeneratorOptions } from "./password-generator-options"; import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; @@ -341,24 +341,6 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId }); } - normalizeOptions( - options: PasswordGeneratorOptions, - enforcedPolicyOptions: PasswordGeneratorPolicyOptions, - ) { - const evaluator = - options.type == "password" - ? new PasswordGeneratorOptionsEvaluator(enforcedPolicyOptions) - : new PassphraseGeneratorOptionsEvaluator(enforcedPolicyOptions); - - const evaluatedOptions = evaluator.applyPolicy(options); - const santizedOptions = evaluator.sanitize(evaluatedOptions); - - // callers assume this function updates the options parameter - Object.assign(options, santizedOptions); - - return options; - } - private capitalize(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } diff --git a/libs/common/src/tools/generator/password/password-generator-options.ts b/libs/common/src/tools/generator/password/password-generator-options.ts index a0b42b3032..aa0a6f7dab 100644 --- a/libs/common/src/tools/generator/password/password-generator-options.ts +++ b/libs/common/src/tools/generator/password/password-generator-options.ts @@ -1,3 +1,4 @@ +import { GeneratorNavigation } from "../navigation/generator-navigation"; import { PassphraseGenerationOptions } from "../passphrase/passphrase-generation-options"; import { PasswordGenerationOptions } from "./password-generation-options"; @@ -6,12 +7,5 @@ import { PasswordGenerationOptions } from "./password-generation-options"; * This type includes all properties suitable for reactive data binding. */ export type PasswordGeneratorOptions = PasswordGenerationOptions & - PassphraseGenerationOptions & { - /** The algorithm to use for credential generation. - * Properties on @see PasswordGenerationOptions should be processed - * only when `type === "password"`. - * Properties on @see PassphraseGenerationOptions should be processed - * only when `type === "passphrase"`. - */ - type?: "password" | "passphrase"; - }; + PassphraseGenerationOptions & + GeneratorNavigation; diff --git a/libs/common/src/tools/generator/password/password-generator-policy.spec.ts b/libs/common/src/tools/generator/password/password-generator-policy.spec.ts new file mode 100644 index 0000000000..206d88741b --- /dev/null +++ b/libs/common/src/tools/generator/password/password-generator-policy.spec.ts @@ -0,0 +1,55 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; +import { PolicyId } from "../../../types/guid"; + +import { DisabledPasswordGeneratorPolicy, leastPrivilege } from "./password-generator-policy"; + +function createPolicy( + data: any, + type: PolicyType = PolicyType.PasswordGenerator, + enabled: boolean = true, +) { + return new Policy({ + id: "id" as PolicyId, + organizationId: "organizationId", + data, + enabled, + type, + }); +} + +describe("leastPrivilege", () => { + it("should return the accumulator when the policy type does not apply", () => { + const policy = createPolicy({}, PolicyType.RequireSso); + + const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPasswordGeneratorPolicy); + }); + + it("should return the accumulator when the policy is not enabled", () => { + const policy = createPolicy({}, PolicyType.PasswordGenerator, false); + + const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); + + expect(result).toEqual(DisabledPasswordGeneratorPolicy); + }); + + it.each([ + ["minLength", 10, "minLength"], + ["useUpper", true, "useUppercase"], + ["useLower", true, "useLowercase"], + ["useNumbers", true, "useNumbers"], + ["minNumbers", 10, "numberCount"], + ["useSpecial", true, "useSpecial"], + ["minSpecial", 10, "specialCount"], + ])("should take the %p from the policy", (input, value, expected) => { + const policy = createPolicy({ [input]: value }); + + const result = leastPrivilege(DisabledPasswordGeneratorPolicy, policy); + + expect(result).toEqual({ ...DisabledPasswordGeneratorPolicy, [expected]: value }); + }); +}); diff --git a/libs/common/src/tools/generator/password/password-generator-policy.ts b/libs/common/src/tools/generator/password/password-generator-policy.ts index c28631e9de..7de6b49788 100644 --- a/libs/common/src/tools/generator/password/password-generator-policy.ts +++ b/libs/common/src/tools/generator/password/password-generator-policy.ts @@ -1,3 +1,8 @@ +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; + /** Policy options enforced during password generation. */ export type PasswordGeneratorPolicy = { /** The minimum length of generated passwords. @@ -48,3 +53,25 @@ export const DisabledPasswordGeneratorPolicy: PasswordGeneratorPolicy = Object.f useSpecial: false, specialCount: 0, }); + +/** Reduces a policy into an accumulator by accepting the most restrictive + * values from each policy. + * @param acc the accumulator + * @param policy the policy to reduce + * @returns the most restrictive values between the policy and accumulator. + */ +export function leastPrivilege(acc: PasswordGeneratorPolicy, policy: Policy) { + if (policy.type !== PolicyType.PasswordGenerator || !policy.enabled) { + return acc; + } + + return { + minLength: Math.max(acc.minLength, policy.data.minLength ?? acc.minLength), + useUppercase: policy.data.useUpper || acc.useUppercase, + useLowercase: policy.data.useLower || acc.useLowercase, + useNumbers: policy.data.useNumbers || acc.useNumbers, + numberCount: Math.max(acc.numberCount, policy.data.minNumbers ?? acc.numberCount), + useSpecial: policy.data.useSpecial || acc.useSpecial, + specialCount: Math.max(acc.specialCount, policy.data.minSpecial ?? acc.specialCount), + }; +} diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts index 6c213f8c54..5efc6a85a7 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.spec.ts @@ -4,6 +4,7 @@ */ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -16,6 +17,7 @@ import { PASSWORD_SETTINGS } from "../key-definitions"; import { DisabledPasswordGeneratorPolicy } from "./password-generator-policy"; import { + DefaultPasswordGenerationOptions, PasswordGenerationServiceAbstraction, PasswordGeneratorOptionsEvaluator, PasswordGeneratorStrategy, @@ -24,17 +26,8 @@ import { const SomeUser = "some user" as UserId; describe("Password generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new PasswordGeneratorStrategy(null, null); - const policy = mock({ - type: PolicyType.DisableSend, - }); - - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); - - it("should map to the policy evaluator", () => { + describe("toEvaluator()", () => { + it("should map to a password policy evaluator", async () => { const strategy = new PasswordGeneratorStrategy(null, null); const policy = mock({ type: PolicyType.PasswordGenerator, @@ -49,7 +42,8 @@ describe("Password generation strategy", () => { }, }); - const evaluator = strategy.evaluator(policy); + const evaluator$ = of([policy]).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); expect(evaluator.policy).toMatchObject({ @@ -63,13 +57,18 @@ describe("Password generation strategy", () => { }); }); - it("should map `null` to a default policy evaluator", () => { - const strategy = new PasswordGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); + it.each([[[]], [null], [undefined]])( + "should map `%p` to a disabled password policy evaluator", + async (policies) => { + const strategy = new PasswordGeneratorStrategy(null, null); - expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); - expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy); - }); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); + + expect(evaluator).toBeInstanceOf(PasswordGeneratorOptionsEvaluator); + expect(evaluator.policy).toMatchObject(DisabledPasswordGeneratorPolicy); + }, + ); }); describe("durableState", () => { @@ -84,6 +83,16 @@ describe("Password generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new PasswordGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultPasswordGenerationOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock(); diff --git a/libs/common/src/tools/generator/password/password-generator-strategy.ts b/libs/common/src/tools/generator/password/password-generator-strategy.ts index 223470c586..e98ae6fb16 100644 --- a/libs/common/src/tools/generator/password/password-generator-strategy.ts +++ b/libs/common/src/tools/generator/password/password-generator-strategy.ts @@ -1,18 +1,22 @@ +import { BehaviorSubject, map, pipe } from "rxjs"; + import { GeneratorStrategy } from ".."; import { PolicyType } from "../../../admin-console/enums"; -// FIXME: use index.ts imports once policy abstractions and models -// implement ADR-0002 -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { PasswordGenerationServiceAbstraction } from "../abstractions/password-generation.service.abstraction"; import { PASSWORD_SETTINGS } from "../key-definitions"; +import { reduceCollection } from "../reduce-collection.operator"; -import { PasswordGenerationOptions } from "./password-generation-options"; -import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; +import { + DefaultPasswordGenerationOptions, + PasswordGenerationOptions, +} from "./password-generation-options"; import { PasswordGeneratorOptionsEvaluator } from "./password-generator-options-evaluator"; import { DisabledPasswordGeneratorPolicy, PasswordGeneratorPolicy, + leastPrivilege, } from "./password-generator-policy"; const ONE_MINUTE = 60 * 1000; @@ -34,6 +38,11 @@ export class PasswordGeneratorStrategy return this.stateProvider.getUser(id, PASSWORD_SETTINGS); } + /** Gets the default options. */ + defaults$(_: UserId) { + return new BehaviorSubject({ ...DefaultPasswordGenerationOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { return PolicyType.PasswordGenerator; @@ -43,26 +52,12 @@ export class PasswordGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy): PasswordGeneratorOptionsEvaluator { - if (!policy) { - return new PasswordGeneratorOptionsEvaluator(DisabledPasswordGeneratorPolicy); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new PasswordGeneratorOptionsEvaluator({ - minLength: policy.data.minLength, - useUppercase: policy.data.useUpper, - useLowercase: policy.data.useLower, - useNumbers: policy.data.useNumbers, - numberCount: policy.data.minNumbers, - useSpecial: policy.data.useSpecial, - specialCount: policy.data.minSpecial, - }); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe( + reduceCollection(leastPrivilege, DisabledPasswordGeneratorPolicy), + map((policy) => new PasswordGeneratorOptionsEvaluator(policy)), + ); } /** {@link GeneratorStrategy.generate} */ diff --git a/libs/common/src/tools/generator/reduce-collection.operator.spec.ts b/libs/common/src/tools/generator/reduce-collection.operator.spec.ts new file mode 100644 index 0000000000..49648dfdf0 --- /dev/null +++ b/libs/common/src/tools/generator/reduce-collection.operator.spec.ts @@ -0,0 +1,33 @@ +/** + * include structuredClone in test environment. + * @jest-environment ../../../../shared/test.environment.ts + */ + +import { of, firstValueFrom } from "rxjs"; + +import { reduceCollection } from "./reduce-collection.operator"; + +describe("reduceCollection", () => { + it.each([[null], [undefined], [[]]])( + "should return the default value when the collection is %p", + async (value: number[]) => { + const reduce = (acc: number, value: number) => acc + value; + const source$ = of(value); + + const result$ = source$.pipe(reduceCollection(reduce, 100)); + const result = await firstValueFrom(result$); + + expect(result).toEqual(100); + }, + ); + + it("should reduce the collection to a single value", async () => { + const reduce = (acc: number, value: number) => acc + value; + const source$ = of([1, 2, 3]); + + const result$ = source$.pipe(reduceCollection(reduce, 0)); + const result = await firstValueFrom(result$); + + expect(result).toEqual(6); + }); +}); diff --git a/libs/common/src/tools/generator/reduce-collection.operator.ts b/libs/common/src/tools/generator/reduce-collection.operator.ts new file mode 100644 index 0000000000..224595eeba --- /dev/null +++ b/libs/common/src/tools/generator/reduce-collection.operator.ts @@ -0,0 +1,20 @@ +import { map, OperatorFunction } from "rxjs"; + +/** + * An observable operator that reduces an emitted collection to a single object, + * returning a default if all items are ignored. + * @param reduce The reduce function to apply to the filtered collection. The + * first argument is the accumulator, and the second is the current item. The + * return value is the new accumulator. + * @param defaultValue The default value to return if the collection is empty. The + * default value is also the initial value of the accumulator. + */ +export function reduceCollection( + reduce: (acc: Accumulator, value: Item) => Accumulator, + defaultValue: Accumulator, +): OperatorFunction { + return map((values: Item[]) => { + const reduced = (values ?? []).reduce(reduce, structuredClone(defaultValue)); + return reduced; + }); +} diff --git a/libs/common/src/tools/generator/state/buffered-key-definition.spec.ts b/libs/common/src/tools/generator/state/buffered-key-definition.spec.ts new file mode 100644 index 0000000000..b056cba397 --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-key-definition.spec.ts @@ -0,0 +1,119 @@ +import { GENERATOR_DISK, UserKeyDefinition } from "../../../platform/state"; + +import { BufferedKeyDefinition } from "./buffered-key-definition"; + +describe("BufferedKeyDefinition", () => { + const deserializer = (jsonValue: number) => jsonValue + 1; + + describe("toKeyDefinition", () => { + it("should create a key definition", () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + cleanupDelayMs: 5, + clearOn: [], + }); + + const result = key.toKeyDefinition(); + + expect(result).toBeInstanceOf(UserKeyDefinition); + expect(result.stateDefinition).toBe(GENERATOR_DISK); + expect(result.key).toBe("test"); + expect(result.deserializer(1)).toEqual(2); + expect(result.cleanupDelayMs).toEqual(5); + }); + }); + + describe("shouldOverwrite", () => { + it("should call the shouldOverwrite function when its defined", async () => { + const shouldOverwrite = jest.fn(() => true); + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + shouldOverwrite, + clearOn: [], + }); + + const result = await key.shouldOverwrite(true); + + expect(shouldOverwrite).toHaveBeenCalledWith(true); + expect(result).toStrictEqual(true); + }); + + it("should return true when shouldOverwrite is not defined and the input is truthy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + clearOn: [], + }); + + const result = await key.shouldOverwrite(1); + + expect(result).toStrictEqual(true); + }); + + it("should return false when shouldOverwrite is not defined and the input is falsy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + clearOn: [], + }); + + const result = await key.shouldOverwrite(0); + + expect(result).toStrictEqual(false); + }); + }); + + describe("map", () => { + it("should call the map function when its defined", async () => { + const map = jest.fn((value: number) => Promise.resolve(`${value}`)); + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + map, + clearOn: [], + }); + + const result = await key.map(1, true); + + expect(map).toHaveBeenCalledWith(1, true); + expect(result).toStrictEqual("1"); + }); + + it("should fall back to an identity function when map is not defined", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { deserializer, clearOn: [] }); + + const result = await key.map(1, null); + + expect(result).toStrictEqual(1); + }); + }); + + describe("isValid", () => { + it("should call the isValid function when its defined", async () => { + const isValid = jest.fn(() => Promise.resolve(true)); + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { + deserializer, + isValid, + clearOn: [], + }); + + const result = await key.isValid(1, true); + + expect(isValid).toHaveBeenCalledWith(1, true); + expect(result).toStrictEqual(true); + }); + + it("should return true when isValid is not defined and the input is truthy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { deserializer, clearOn: [] }); + + const result = await key.isValid(1, null); + + expect(result).toStrictEqual(true); + }); + + it("should return false when isValid is not defined and the input is falsy", async () => { + const key = new BufferedKeyDefinition(GENERATOR_DISK, "test", { deserializer, clearOn: [] }); + + const result = await key.isValid(0, null); + + expect(result).toStrictEqual(false); + }); + }); +}); diff --git a/libs/common/src/tools/generator/state/buffered-key-definition.ts b/libs/common/src/tools/generator/state/buffered-key-definition.ts new file mode 100644 index 0000000000..5457410f80 --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-key-definition.ts @@ -0,0 +1,100 @@ +import { UserKeyDefinition, UserKeyDefinitionOptions } from "../../../platform/state"; +// eslint-disable-next-line -- `StateDefinition` used as an argument +import { StateDefinition } from "../../../platform/state/state-definition"; + +/** A set of options for customizing the behavior of a {@link BufferedKeyDefinition} + */ +export type BufferedKeyDefinitionOptions = + UserKeyDefinitionOptions & { + /** Checks whether the input type can be converted to the output type. + * @param input the data that is rolling over. + * @returns `true` if the definition is valid, otherwise `false`. If this + * function is not specified, any truthy input is valid. + * + * @remarks this is intended for cases where you're working with validated or + * signed data. It should be used to prevent data from being "laundered" through + * synchronized state. + */ + isValid?: (input: Input, dependency: Dependency) => Promise; + + /** Transforms the input data format to its output format. + * @param input the data that is rolling over. + * @returns the converted value. If this function is not specified, the value + * is asserted as the output type. + * + * @remarks This is intended for converting between, say, a replication format + * and a disk format or rotating encryption keys. + */ + map?: (input: Input, dependency: Dependency) => Promise; + + /** Checks whether an overwrite should occur + * @param dependency the latest value from the dependency observable provided + * to the buffered state. + * @returns `true` if a overwrite should occur, otherwise `false`. If this + * function is not specified, overwrites occur when the dependency is truthy. + * + * @remarks This is intended for waiting to overwrite until a dependency becomes + * available (e.g. an encryption key or a user confirmation). + */ + shouldOverwrite?: (dependency: Dependency) => boolean; + }; + +/** Storage and mapping settings for data stored by a `BufferedState`. + */ +export class BufferedKeyDefinition { + /** + * Defines a buffered state + * @param stateDefinition The domain of the buffer + * @param key Domain key that identifies the buffered value. This key must + * not be reused in any capacity. + * @param options Configures the operation of the buffered state. + */ + constructor( + readonly stateDefinition: StateDefinition, + readonly key: string, + readonly options: BufferedKeyDefinitionOptions, + ) {} + + /** Converts the buffered key definition to a state provider + * key definition + */ + toKeyDefinition() { + const bufferedKey = new UserKeyDefinition(this.stateDefinition, this.key, this.options); + + return bufferedKey; + } + + /** Checks whether the dependency triggers an overwrite. */ + shouldOverwrite(dependency: Dependency) { + const shouldOverwrite = this.options?.shouldOverwrite; + if (shouldOverwrite) { + return shouldOverwrite(dependency); + } + + return dependency ? true : false; + } + + /** Converts the input data format to its output format. + * @returns the converted value. + */ + map(input: Input, dependency: Dependency) { + const map = this.options?.map; + if (map) { + return map(input, dependency); + } + + return Promise.resolve(input as unknown as Output); + } + + /** Checks whether the input type can be converted to the output type. + * @returns `true` if the definition is valid, otherwise `false`. + */ + isValid(input: Input, dependency: Dependency) { + const isValid = this.options?.isValid; + if (isValid) { + return isValid(input, dependency); + } + + return Promise.resolve(input ? true : false); + } +} diff --git a/libs/common/src/tools/generator/state/buffered-state.spec.ts b/libs/common/src/tools/generator/state/buffered-state.spec.ts new file mode 100644 index 0000000000..7f9722d384 --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-state.spec.ts @@ -0,0 +1,375 @@ +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; + +import { + mockAccountServiceWith, + FakeStateProvider, + awaitAsync, + trackEmissions, +} from "../../../../spec"; +import { GENERATOR_DISK, KeyDefinition } from "../../../platform/state"; +import { UserId } from "../../../types/guid"; + +import { BufferedKeyDefinition } from "./buffered-key-definition"; +import { BufferedState } from "./buffered-state"; + +const SomeUser = "SomeUser" as UserId; +const accountService = mockAccountServiceWith(SomeUser); +type SomeType = { foo: boolean; bar: boolean }; + +const SOME_KEY = new KeyDefinition(GENERATOR_DISK, "fooBar", { + deserializer: (jsonValue) => jsonValue as SomeType, +}); +const BUFFER_KEY = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + clearOn: [], +}); + +describe("BufferedState", () => { + describe("state$", function () { + it("reads from the output state", async () => { + const provider = new FakeStateProvider(accountService); + const value = { foo: true, bar: false }; + const outputState = provider.getUser(SomeUser, SOME_KEY); + await outputState.update(() => value); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(value); + }); + + it("updates when the output state updates", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await outputState.update(() => secondValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, secondValue]); + }); + + // this test is important for data migrations, which set + // the buffered state without using the `BufferedState` abstraction. + it.each([[null], [undefined]])( + "reads from the output state when the buffered state is '%p'", + async (bufferValue) => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferValue, SomeUser); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(firstValue); + }, + ); + + // also important for data migrations + it("rolls over pending values from the buffered state immediately by default", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + await outputState.update(() => ({ foo: true, bar: false })); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferedValue, SomeUser); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(bufferedValue); + }); + + // also important for data migrations + it("reads from the output state when its dependency is false", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const value = { foo: true, bar: false }; + await outputState.update(() => value); + const dependency = new BehaviorSubject(false).asObservable(); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState, dependency); + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), { foo: true, bar: true }, SomeUser); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(value); + }); + + // also important for data migrations + it("overwrites the output state when its dependency emits a truthy value", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(false); + const bufferedState = new BufferedState( + provider, + BUFFER_KEY, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferedValue, SomeUser); + + const result = trackEmissions(bufferedState.state$); + dependency.next(true); + await awaitAsync(); + + expect(result).toEqual([firstValue, bufferedValue]); + }); + + it("overwrites the output state when shouldOverwrite returns a truthy value", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + shouldOverwrite: () => true, + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + await outputState.update(() => ({ foo: true, bar: false })); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(bufferedKey.toKeyDefinition(), bufferedValue, SomeUser); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(bufferedValue); + }); + + it("reads from the output state when shouldOverwrite returns a falsy value", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + shouldOverwrite: () => false, + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const value = { foo: true, bar: false }; + await outputState.update(() => value); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + await provider.setUserState( + bufferedKey.toKeyDefinition(), + { foo: true, bar: true }, + SomeUser, + ); + + const result = await firstValueFrom(bufferedState.state$); + + expect(result).toEqual(value); + }); + + it("replaces the output state when shouldOverwrite transforms its dependency to a truthy value", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + shouldOverwrite: (dependency) => !dependency, + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(true); + const bufferedState = new BufferedState( + provider, + bufferedKey, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + await provider.setUserState(bufferedKey.toKeyDefinition(), bufferedValue, SomeUser); + + const result = trackEmissions(bufferedState.state$); + dependency.next(false); + await awaitAsync(); + + expect(result).toEqual([firstValue, bufferedValue]); + }); + }); + + describe("userId", () => { + const AnotherUser = "anotherUser" as UserId; + + it.each([[SomeUser], [AnotherUser]])("gets the userId", (userId) => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(userId, SOME_KEY); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = bufferedState.userId; + + expect(result).toEqual(userId); + }); + }); + + describe("update", () => { + it("updates state$", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.update(() => secondValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, secondValue]); + }); + + it("respects update options", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.update(() => secondValue, { + shouldUpdate: (_, latest) => latest, + combineLatestWith: of(false), + }); + await awaitAsync(); + + expect(result).toEqual([firstValue]); + }); + }); + + describe("buffer", () => { + it("updates state$ once per overwrite", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + const secondValue = { foo: true, bar: true }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(secondValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, secondValue]); + }); + + it("emits the output state when its dependency is false", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(false); + const bufferedState = new BufferedState( + provider, + BUFFER_KEY, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, firstValue]); + }); + + it("replaces the output state when its dependency becomes true", async () => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const dependency = new BehaviorSubject(false); + const bufferedState = new BufferedState( + provider, + BUFFER_KEY, + outputState, + dependency.asObservable(), + ); + const bufferedValue = { foo: true, bar: true }; + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + dependency.next(true); + await awaitAsync(); + + expect(result).toEqual([firstValue, firstValue, bufferedValue]); + }); + + it.each([[null], [undefined]])("ignores `%p`", async (bufferedValue) => { + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + await awaitAsync(); + + expect(result).toEqual([firstValue]); + }); + + it("discards the buffered data when isValid returns false", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + isValid: () => Promise.resolve(false), + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer({ foo: true, bar: true }); + await awaitAsync(); + + expect(result).toEqual([firstValue, firstValue]); + }); + + it("overwrites the output when isValid returns true", async () => { + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + isValid: () => Promise.resolve(true), + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + const bufferedValue = { foo: true, bar: true }; + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer(bufferedValue); + await awaitAsync(); + + expect(result).toEqual([firstValue, bufferedValue]); + }); + + it("maps the buffered data when it overwrites the state", async () => { + const mappedValue = { foo: true, bar: true }; + const bufferedKey = new BufferedKeyDefinition(GENERATOR_DISK, "fooBar_buffer", { + deserializer: (jsonValue) => jsonValue as SomeType, + map: () => Promise.resolve(mappedValue), + clearOn: [], + }); + const provider = new FakeStateProvider(accountService); + const outputState = provider.getUser(SomeUser, SOME_KEY); + const firstValue = { foo: true, bar: false }; + await outputState.update(() => firstValue); + const bufferedState = new BufferedState(provider, bufferedKey, outputState); + + const result = trackEmissions(bufferedState.state$); + await bufferedState.buffer({ foo: false, bar: false }); + await awaitAsync(); + + expect(result).toEqual([firstValue, mappedValue]); + }); + }); +}); diff --git a/libs/common/src/tools/generator/state/buffered-state.ts b/libs/common/src/tools/generator/state/buffered-state.ts new file mode 100644 index 0000000000..42b14b815c --- /dev/null +++ b/libs/common/src/tools/generator/state/buffered-state.ts @@ -0,0 +1,144 @@ +import { Observable, combineLatest, concatMap, filter, map, of } from "rxjs"; + +import { + StateProvider, + SingleUserState, + CombinedState, + StateUpdateOptions, +} from "../../../platform/state"; + +import { BufferedKeyDefinition } from "./buffered-key-definition"; + +/** Stateful storage that overwrites one state with a buffered state. + * When a overwrite occurs, the input state is automatically deleted. + * @remarks The buffered state can only overwrite non-nullish values. If the + * buffer key contains `null` or `undefined`, it will do nothing. + */ +export class BufferedState implements SingleUserState { + /** + * Instantiate a buffered state + * @param provider constructs the buffer. + * @param key defines the buffer location. + * @param output updates when a overwrite occurs + * @param dependency$ provides data the buffer depends upon to evaluate and + * transform its data. If this is omitted, then `true` is injected as + * a dependency, which with a default output will trigger a overwrite immediately. + * + * @remarks `dependency$` enables overwrite control during dynamic circumstances, + * such as when a overwrite should occur only if a user key is available. + */ + constructor( + provider: StateProvider, + private key: BufferedKeyDefinition, + private output: SingleUserState, + dependency$: Observable = null, + ) { + this.bufferState = provider.getUser(output.userId, key.toKeyDefinition()); + + const watching = [ + this.bufferState.state$, + this.output.state$, + dependency$ ?? of(true as unknown as Dependency), + ] as const; + + this.state$ = combineLatest(watching).pipe( + concatMap(async ([input, output, dependency]) => { + const normalized = input ?? null; + + const canOverwrite = normalized !== null && key.shouldOverwrite(dependency); + if (canOverwrite) { + await this.updateOutput(dependency); + + // prevent duplicate updates by suppressing the update + return [false, output] as const; + } + + return [true, output] as const; + }), + filter(([updated]) => updated), + map(([, output]) => output), + ); + + this.combinedState$ = this.state$.pipe(map((state) => [this.output.userId, state])); + + this.bufferState$ = this.bufferState.state$; + } + + private bufferState: SingleUserState; + + private async updateOutput(dependency: Dependency) { + // retrieve the latest input value + let input: Input; + await this.bufferState.update((state) => state, { + shouldUpdate: (state) => { + input = state; + return false; + }, + }); + + // bail if this update lost the race with the last update + if (input === null) { + return; + } + + // destroy invalid data and bail + if (!(await this.key.isValid(input, dependency))) { + await this.bufferState.update(() => null); + return; + } + + // overwrite anything left to the output; the updates need to be awaited with `Promise.all` + // so that `inputState.update(() => null)` runs before `shouldUpdate` reads the value (above). + // This lets the emission from `this.outputState.update` renter the `concatMap`. If the + // awaits run in sequence, it can win the race and cause a double emission. + const output = await this.key.map(input, dependency); + await Promise.all([this.output.update(() => output), this.bufferState.update(() => null)]); + + return; + } + + /** {@link SingleUserState.userId} */ + get userId() { + return this.output.userId; + } + + /** Observes changes to the output state. This updates when the output + * state updates, when the buffer is moved to the output, and when `BufferedState.buffer` + * is invoked. + */ + readonly state$: Observable; + + /** {@link SingleUserState.combinedState$} */ + readonly combinedState$: Observable>; + + /** Buffers a value state. The buffered state overwrites the output + * state when a subscription occurs. + * @param value the state to roll over. Setting this to `null` or `undefined` + * has no effect. + */ + async buffer(value: Input): Promise { + const normalized = value ?? null; + if (normalized !== null) { + await this.bufferState.update(() => normalized); + } + } + + /** The data presently being buffered. This emits the pending value each time + * new buffer data is provided. It emits null when the buffer is empty. + */ + readonly bufferState$: Observable; + + /** Updates the output state. + * @param configureState a callback that returns an updated output + * state. The callback receives the state's present value as its + * first argument and the dependencies listed in `options.combinedLatestWith` + * as its second argument. + * @param options configures how the update is applied. See {@link StateUpdateOptions}. + */ + update( + configureState: (state: Output, dependencies: TCombine) => Output, + options: StateUpdateOptions = null, + ): Promise { + return this.output.update(configureState, options); + } +} diff --git a/libs/common/src/tools/generator/state/classified-format.ts b/libs/common/src/tools/generator/state/classified-format.ts new file mode 100644 index 0000000000..93147a0fb5 --- /dev/null +++ b/libs/common/src/tools/generator/state/classified-format.ts @@ -0,0 +1,19 @@ +import { Jsonify } from "type-fest"; + +/** Describes the structure of data stored by the SecretState's + * encrypted state. Notably, this interface ensures that `Disclosed` + * round trips through JSON serialization. It also preserves the + * Id. + */ +export type ClassifiedFormat = { + /** Identifies records. `null` when storing a `value` */ + readonly id: Id | null; + /** Serialized {@link EncString} of the secret state's + * secret-level classified data. + */ + readonly secret: string; + /** serialized representation of the secret state's + * disclosed-level classified data. + */ + readonly disclosed: Jsonify; +}; diff --git a/libs/common/src/tools/generator/state/data-packer.abstraction.ts b/libs/common/src/tools/generator/state/data-packer.abstraction.ts index cb712e0fd9..439fbb66c8 100644 --- a/libs/common/src/tools/generator/state/data-packer.abstraction.ts +++ b/libs/common/src/tools/generator/state/data-packer.abstraction.ts @@ -9,7 +9,7 @@ export abstract class DataPacker { * @param value is packed into the string * @returns the packed string */ - abstract pack(value: Data): string; + abstract pack(value: Jsonify): string; /** Unpacks a string produced by pack. * @param packedValue is the string to unpack diff --git a/libs/common/src/tools/generator/state/padded-data-packer.spec.ts b/libs/common/src/tools/generator/state/padded-data-packer.spec.ts index 3cf225026b..7e1d506988 100644 --- a/libs/common/src/tools/generator/state/padded-data-packer.spec.ts +++ b/libs/common/src/tools/generator/state/padded-data-packer.spec.ts @@ -88,14 +88,4 @@ describe("UserKeyEncryptor", () => { expect(unpacked).toEqual(input); }); - - it("should unpack a packed JSON-serializable value", () => { - const dataPacker = new PaddedDataPacker(8); - const input = { foo: new Date(100) }; - - const packed = dataPacker.pack(input); - const unpacked = dataPacker.unpack(packed); - - expect(unpacked).toEqual({ foo: "1970-01-01T00:00:00.100Z" }); - }); }); diff --git a/libs/common/src/tools/generator/state/padded-data-packer.ts b/libs/common/src/tools/generator/state/padded-data-packer.ts index b55dfa378b..d1573e5cb7 100644 --- a/libs/common/src/tools/generator/state/padded-data-packer.ts +++ b/libs/common/src/tools/generator/state/padded-data-packer.ts @@ -37,7 +37,7 @@ export class PaddedDataPacker extends DataPackerAbstraction { * with the frameSize. * @see {@link DataPackerAbstraction.unpack} */ - pack(value: Secret) { + pack(value: Jsonify) { // encode the value const json = JSON.stringify(value); const b64 = Utils.fromUtf8ToB64(json); @@ -58,11 +58,12 @@ export class PaddedDataPacker extends DataPackerAbstraction { /** {@link DataPackerAbstraction.unpack} */ unpack(secret: string): Jsonify { // frame size is stored before the JSON payload in base 10 - const frameBreakpoint = secret.indexOf(DATA_PACKING.divider); - if (frameBreakpoint < 1) { + const frameEndIndex = secret.indexOf(DATA_PACKING.divider); + if (frameEndIndex < 1) { throw new Error("missing frame size"); } - const frameSize = parseInt(secret.slice(0, frameBreakpoint), 10); + const frameSize = parseInt(secret.slice(0, frameEndIndex), 10); + const dataStartIndex = frameEndIndex + 1; // The decrypted string should be a multiple of the frame length if (secret.length % frameSize > 0) { @@ -70,20 +71,20 @@ export class PaddedDataPacker extends DataPackerAbstraction { } // encoded data terminates with the divider, followed by the padding character - const jsonBreakpoint = secret.lastIndexOf(DATA_PACKING.divider); - if (jsonBreakpoint == frameBreakpoint) { + const dataEndIndex = secret.lastIndexOf(DATA_PACKING.divider); + if (dataEndIndex == frameEndIndex) { throw new Error("missing json object"); } - const paddingBegins = jsonBreakpoint + 1; + const paddingStartIndex = dataEndIndex + 1; // If the padding contains invalid padding characters then the padding could be used // as a side channel for arbitrary data. - if (secret.slice(paddingBegins).match(DATA_PACKING.hasInvalidPadding)) { + if (secret.slice(paddingStartIndex).match(DATA_PACKING.hasInvalidPadding)) { throw new Error("invalid padding"); } // remove frame size and padding - const b64 = secret.substring(frameBreakpoint, paddingBegins); + const b64 = secret.slice(dataStartIndex, dataEndIndex); // unpack the stored data const json = Utils.fromB64ToUtf8(b64); diff --git a/libs/common/src/tools/generator/state/secret-classifier.spec.ts b/libs/common/src/tools/generator/state/secret-classifier.spec.ts index 819cd10923..41dd8dc71b 100644 --- a/libs/common/src/tools/generator/state/secret-classifier.spec.ts +++ b/libs/common/src/tools/generator/state/secret-classifier.spec.ts @@ -77,6 +77,15 @@ describe("SecretClassifier", () => { expect(classified.disclosed).toEqual({ foo: true }); }); + it("jsonifies its outputs", () => { + const classifier = SecretClassifier.allSecret<{ foo: Date; bar: Date }>().disclose("foo"); + + const classified = classifier.classify({ foo: new Date(100), bar: new Date(100) }); + + expect(classified.disclosed).toEqual({ foo: "1970-01-01T00:00:00.100Z" }); + expect(classified.secret).toEqual({ bar: "1970-01-01T00:00:00.100Z" }); + }); + it("deletes disclosed properties from the secret member", () => { const classifier = SecretClassifier.allSecret<{ foo: boolean; bar: boolean }>().disclose( "foo", @@ -106,15 +115,6 @@ describe("SecretClassifier", () => { expect(classified.disclosed).toEqual({}); }); - - it("returns its input as the secret member", () => { - const classifier = SecretClassifier.allSecret<{ foo: boolean }>(); - const input = { foo: true }; - - const classified = classifier.classify(input); - - expect(classified.secret).toEqual(input); - }); }); describe("declassify", () => { diff --git a/libs/common/src/tools/generator/state/secret-classifier.ts b/libs/common/src/tools/generator/state/secret-classifier.ts index 232a31c686..a26b01ac5d 100644 --- a/libs/common/src/tools/generator/state/secret-classifier.ts +++ b/libs/common/src/tools/generator/state/secret-classifier.ts @@ -77,17 +77,19 @@ export class SecretClassifier { } /** Partitions `secret` into its disclosed properties and secret properties. - * @param secret The object to partition + * @param value The object to partition * @returns an object that classifies secrets. * The `disclosed` member is new and contains disclosed properties. - * The `secret` member aliases the secret parameter, with all - * disclosed and excluded properties deleted. + * The `secret` member is a copy of the secret parameter, including its + * prototype, with all disclosed and excluded properties deleted. */ - classify(secret: Plaintext): { disclosed: Disclosed; secret: Secret } { - const copy = { ...secret }; + classify(value: Plaintext): { disclosed: Jsonify<Disclosed>; secret: Jsonify<Secret> } { + // need to JSONify during classification because the prototype is almost guaranteed + // to be invalid when this method deletes arbitrary properties. + const secret = JSON.parse(JSON.stringify(value)) as Record<keyof Plaintext, unknown>; for (const excludedProp of this.excluded) { - delete copy[excludedProp]; + delete secret[excludedProp]; } const disclosed: Record<PropertyKey, unknown> = {}; @@ -95,13 +97,13 @@ export class SecretClassifier<Plaintext extends object, Disclosed, Secret> { // disclosedProp is known to be a subset of the keys of `Plaintext`, so these // type assertions are accurate. // FIXME: prove it to the compiler - disclosed[disclosedProp] = copy[disclosedProp as unknown as keyof Plaintext]; - delete copy[disclosedProp as unknown as keyof Plaintext]; + disclosed[disclosedProp] = secret[disclosedProp as keyof Plaintext]; + delete secret[disclosedProp as keyof Plaintext]; } return { - disclosed: disclosed as Disclosed, - secret: copy as unknown as Secret, + disclosed: disclosed as Jsonify<Disclosed>, + secret: secret as Jsonify<Secret>, }; } diff --git a/libs/common/src/tools/generator/state/secret-key-definition.spec.ts b/libs/common/src/tools/generator/state/secret-key-definition.spec.ts index 20bc1f5ee1..a347268b0b 100644 --- a/libs/common/src/tools/generator/state/secret-key-definition.spec.ts +++ b/libs/common/src/tools/generator/state/secret-key-definition.spec.ts @@ -1,11 +1,35 @@ -import { GENERATOR_DISK } from "../../../platform/state"; +import { GENERATOR_DISK, UserKeyDefinitionOptions } from "../../../platform/state"; import { SecretClassifier } from "./secret-classifier"; import { SecretKeyDefinition } from "./secret-key-definition"; describe("SecretKeyDefinition", () => { const classifier = SecretClassifier.allSecret<{ foo: boolean }>(); - const options = { deserializer: (v: any) => v }; + const options: UserKeyDefinitionOptions<any> = { deserializer: (v: any) => v, clearOn: [] }; + + it("toEncryptedStateKey returns a key", () => { + const expectedOptions: UserKeyDefinitionOptions<any> = { + deserializer: (v: any) => v, + cleanupDelayMs: 100, + clearOn: ["logout", "lock"], + }; + const definition = SecretKeyDefinition.value( + GENERATOR_DISK, + "key", + classifier, + expectedOptions, + ); + const expectedDeserializerResult = {} as any; + + const result = definition.toEncryptedStateKey(); + const deserializerResult = result.deserializer(expectedDeserializerResult); + + expect(result.stateDefinition).toEqual(GENERATOR_DISK); + expect(result.key).toBe("key"); + expect(result.cleanupDelayMs).toBe(expectedOptions.cleanupDelayMs); + expect(result.clearOn).toEqual(expectedOptions.clearOn); + expect(deserializerResult).toBe(expectedDeserializerResult); + }); describe("value", () => { it("returns an initialized SecretKeyDefinition", () => { diff --git a/libs/common/src/tools/generator/state/secret-key-definition.ts b/libs/common/src/tools/generator/state/secret-key-definition.ts index eb139efbe7..22496d878e 100644 --- a/libs/common/src/tools/generator/state/secret-key-definition.ts +++ b/libs/common/src/tools/generator/state/secret-key-definition.ts @@ -1,6 +1,7 @@ -import { KeyDefinitionOptions } from "../../../platform/state"; +import { UserKeyDefinitionOptions, UserKeyDefinition } from "../../../platform/state"; // eslint-disable-next-line -- `StateDefinition` used as an argument import { StateDefinition } from "../../../platform/state/state-definition"; +import { ClassifiedFormat } from "./classified-format"; import { SecretClassifier } from "./secret-classifier"; /** Encryption and storage settings for data stored by a `SecretState`. @@ -10,7 +11,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec readonly stateDefinition: StateDefinition, readonly key: string, readonly classifier: SecretClassifier<Inner, Disclosed, Secret>, - readonly options: KeyDefinitionOptions<Inner>, + readonly options: UserKeyDefinitionOptions<Inner>, // type erasure is necessary here because typescript doesn't support // higher kinded types that generalize over collections. The invariants // needed to make this typesafe are maintained by the static factories. @@ -18,6 +19,22 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec readonly reconstruct: ([inners, ids]: (readonly [Id, any])[]) => Outer, ) {} + /** Converts the secret key to the `KeyDefinition` used for secret storage. */ + toEncryptedStateKey() { + const secretKey = new UserKeyDefinition<ClassifiedFormat<Id, Disclosed>[]>( + this.stateDefinition, + this.key, + { + cleanupDelayMs: this.options.cleanupDelayMs, + deserializer: (jsonValue) => jsonValue as ClassifiedFormat<Id, Disclosed>[], + // Clear encrypted state on logout + clearOn: this.options.clearOn, + }, + ); + + return secretKey; + } + /** * Define a secret state for a single value * @param stateDefinition The domain of the secret's durable state. @@ -30,7 +47,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec stateDefinition: StateDefinition, key: string, classifier: SecretClassifier<Value, Disclosed, Secret>, - options: KeyDefinitionOptions<Value>, + options: UserKeyDefinitionOptions<Value>, ) { return new SecretKeyDefinition<Value, void, Value, Disclosed, Secret>( stateDefinition, @@ -54,7 +71,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec stateDefinition: StateDefinition, key: string, classifier: SecretClassifier<Item, Disclosed, Secret>, - options: KeyDefinitionOptions<Item>, + options: UserKeyDefinitionOptions<Item>, ) { return new SecretKeyDefinition<Item[], number, Item, Disclosed, Secret>( stateDefinition, @@ -78,7 +95,7 @@ export class SecretKeyDefinition<Outer, Id, Inner extends object, Disclosed, Sec stateDefinition: StateDefinition, key: string, classifier: SecretClassifier<Item, Disclosed, Secret>, - options: KeyDefinitionOptions<Item>, + options: UserKeyDefinitionOptions<Item>, ) { return new SecretKeyDefinition<Record<Id, Item>, Id, Item, Disclosed, Secret>( stateDefinition, diff --git a/libs/common/src/tools/generator/state/secret-state.spec.ts b/libs/common/src/tools/generator/state/secret-state.spec.ts index 364116fed3..1f5e14dde9 100644 --- a/libs/common/src/tools/generator/state/secret-state.spec.ts +++ b/libs/common/src/tools/generator/state/secret-state.spec.ts @@ -36,26 +36,26 @@ const FOOBAR_RECORD = SecretKeyDefinition.record(GENERATOR_DISK, "fooBar", class const SomeUser = "some user" as UserId; -function mockEncryptor(fooBar: FooBar[] = []): UserEncryptor<FooBar> { +function mockEncryptor<T>(fooBar: T[] = []): UserEncryptor { // stores "encrypted values" so that they can be "decrypted" later // while allowing the operations to be interleaved. const encrypted = new Map<string, Jsonify<FooBar>>( - fooBar.map((fb) => [toKey(fb).encryptedString, toValue(fb)] as const), + fooBar.map((fb) => [toKey(fb as any).encryptedString, toValue(fb)] as const), ); - const result = mock<UserEncryptor<FooBar>>({ - encrypt(value: FooBar, user: UserId) { - const encString = toKey(value); + const result = mock<UserEncryptor>({ + encrypt<T>(value: Jsonify<T>, user: UserId) { + const encString = toKey(value as any); encrypted.set(encString.encryptedString, toValue(value)); return Promise.resolve(encString); }, decrypt(secret: EncString, userId: UserId) { - const decString = encrypted.get(toValue(secret.encryptedString)); - return Promise.resolve(decString); + const decValue = encrypted.get(secret.encryptedString); + return Promise.resolve(decValue as any); }, }); - function toKey(value: FooBar) { + function toKey(value: Jsonify<T>) { // `stringify` is only relevant for its uniqueness as a key // to `encrypted`. return makeEncString(JSON.stringify(value)); @@ -68,7 +68,7 @@ function mockEncryptor(fooBar: FooBar[] = []): UserEncryptor<FooBar> { // typescript pops a false positive about missing `encrypt` and `decrypt` // functions, so assert the type manually. - return result as unknown as UserEncryptor<FooBar>; + return result as unknown as UserEncryptor; } async function fakeStateProvider() { @@ -77,7 +77,7 @@ async function fakeStateProvider() { return stateProvider; } -describe("UserEncryptor", () => { +describe("SecretState", () => { describe("from", () => { it("returns a state store", async () => { const provider = await fakeStateProvider(); diff --git a/libs/common/src/tools/generator/state/secret-state.ts b/libs/common/src/tools/generator/state/secret-state.ts index a879b9f788..dc4ee119a6 100644 --- a/libs/common/src/tools/generator/state/secret-state.ts +++ b/libs/common/src/tools/generator/state/secret-state.ts @@ -1,11 +1,7 @@ -import { Observable, concatMap, of, zip, map } from "rxjs"; -import { Jsonify } from "type-fest"; +import { Observable, map, concatMap, share, ReplaySubject, timer } from "rxjs"; import { EncString } from "../../../platform/models/domain/enc-string"; import { - DeriveDefinition, - DerivedState, - KeyDefinition, SingleUserState, StateProvider, StateUpdateOptions, @@ -13,28 +9,11 @@ import { } from "../../../platform/state"; import { UserId } from "../../../types/guid"; +import { ClassifiedFormat } from "./classified-format"; import { SecretKeyDefinition } from "./secret-key-definition"; import { UserEncryptor } from "./user-encryptor.abstraction"; -/** Describes the structure of data stored by the SecretState's - * encrypted state. Notably, this interface ensures that `Disclosed` - * round trips through JSON serialization. It also preserves the - * Id. - * @remarks Tuple representation chosen because it matches - * `Object.entries` format. - */ -type ClassifiedFormat<Id, Disclosed> = { - /** Identifies records. `null` when storing a `value` */ - readonly id: Id | null; - /** Serialized {@link EncString} of the secret state's - * secret-level classified data. - */ - readonly secret: string; - /** serialized representation of the secret state's - * disclosed-level classified data. - */ - readonly disclosed: Jsonify<Disclosed>; -}; +const ONE_MINUTE = 1000 * 60; /** Stores account-specific secrets protected by a UserKeyEncryptor. * @@ -51,17 +30,34 @@ export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret> // wiring the derived and secret states together. private constructor( private readonly key: SecretKeyDefinition<Outer, Id, Plaintext, Disclosed, Secret>, - private readonly encryptor: UserEncryptor<Secret>, - private readonly encrypted: SingleUserState<ClassifiedFormat<Id, Disclosed>[]>, - private readonly plaintext: DerivedState<Outer>, + private readonly encryptor: UserEncryptor, + userId: UserId, + provider: StateProvider, ) { - this.state$ = plaintext.state$; - this.combinedState$ = plaintext.state$.pipe(map((state) => [this.encrypted.userId, state])); + // construct the backing store + this.encryptedState = provider.getUser(userId, key.toEncryptedStateKey()); + + // cache plaintext + this.combinedState$ = this.encryptedState.combinedState$.pipe( + concatMap( + async ([userId, state]) => [userId, await this.declassifyAll(state)] as [UserId, Outer], + ), + share({ + connector: () => { + return new ReplaySubject<[UserId, Outer]>(1); + }, + resetOnRefCountZero: () => timer(key.options.cleanupDelayMs ?? ONE_MINUTE), + }), + ); + + this.state$ = this.combinedState$.pipe(map(([, state]) => state)); } + private readonly encryptedState: SingleUserState<ClassifiedFormat<Id, Disclosed>[]>; + /** {@link SingleUserState.userId} */ get userId() { - return this.encrypted.userId; + return this.encryptedState.userId; } /** Observes changes to the decrypted secret state. The observer @@ -89,67 +85,71 @@ export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret> userId: UserId, key: SecretKeyDefinition<Outer, Id, TFrom, Disclosed, Secret>, provider: StateProvider, - encryptor: UserEncryptor<Secret>, + encryptor: UserEncryptor, ) { - // construct encrypted backing store while avoiding collisions between the derived key and the - // backing storage key. - const secretKey = new KeyDefinition<ClassifiedFormat<Id, Disclosed>[]>( - key.stateDefinition, - key.key, - { - cleanupDelayMs: key.options.cleanupDelayMs, - // FIXME: When the fakes run deserializers and serialization can be guaranteed through - // state providers, decode `jsonValue.secret` instead of it running in `derive`. - deserializer: (jsonValue) => jsonValue as ClassifiedFormat<Id, Disclosed>[], - }, - ); - const encryptedState = provider.getUser(userId, secretKey); - - // construct plaintext store - const plaintextDefinition = DeriveDefinition.from<ClassifiedFormat<Id, Disclosed>[], Outer>( - secretKey, - { - derive: async (from) => { - // fail fast if there's no value - if (from === null || from === undefined) { - return null; - } - - // decrypt each item - const decryptTasks = from.map(async ({ id, secret, disclosed }) => { - const encrypted = EncString.fromJSON(secret); - const decrypted = await encryptor.decrypt(encrypted, encryptedState.userId); - - const declassified = key.classifier.declassify(disclosed, decrypted); - const result = key.options.deserializer(declassified); - - return [id, result] as const; - }); - - // reconstruct expected type - const results = await Promise.all(decryptTasks); - const result = key.reconstruct(results); - - return result; - }, - // wire in the caller's deserializer for memory serialization - deserializer: (d) => { - const items = key.deconstruct(d); - const results = items.map(([k, v]) => [k, key.options.deserializer(v)] as const); - const result = key.reconstruct(results); - return result; - }, - // cache the decrypted data in memory - cleanupDelayMs: key.options.cleanupDelayMs, - }, - ); - const plaintextState = provider.getDerived(encryptedState.state$, plaintextDefinition, null); - - // wrap the encrypted and plaintext states in a `SecretState` facade - const secretState = new SecretState(key, encryptor, encryptedState, plaintextState); + const secretState = new SecretState(key, encryptor, userId, provider); return secretState; } + private async declassifyItem({ id, secret, disclosed }: ClassifiedFormat<Id, Disclosed>) { + const encrypted = EncString.fromJSON(secret); + const decrypted = await this.encryptor.decrypt(encrypted, this.encryptedState.userId); + + const declassified = this.key.classifier.declassify(disclosed, decrypted); + const result = [id, this.key.options.deserializer(declassified)] as const; + + return result; + } + + private async declassifyAll(data: ClassifiedFormat<Id, Disclosed>[]) { + // fail fast if there's no value + if (data === null || data === undefined) { + return null; + } + + // decrypt each item + const decryptTasks = data.map(async (item) => this.declassifyItem(item)); + + // reconstruct expected type + const results = await Promise.all(decryptTasks); + const result = this.key.reconstruct(results); + + return result; + } + + private async classifyItem([id, item]: [Id, Plaintext]) { + const classified = this.key.classifier.classify(item); + const encrypted = await this.encryptor.encrypt(classified.secret, this.encryptedState.userId); + + // the deserializer in the plaintextState's `derive` configuration always runs, but + // `encryptedState` is not guaranteed to serialize the data, so it's necessary to + // round-trip `encrypted` proactively. + const serialized = { + id, + secret: JSON.parse(JSON.stringify(encrypted)), + disclosed: classified.disclosed, + } as ClassifiedFormat<Id, Disclosed>; + + return serialized; + } + + private async classifyAll(data: Outer) { + // fail fast if there's no value + if (data === null || data === undefined) { + return null; + } + + // convert the object to a list format so that all encrypt and decrypt + // operations are self-similar + const desconstructed = this.key.deconstruct(data); + + // encrypt each value individually + const classifyTasks = desconstructed.map(async (item) => this.classifyItem(item)); + const classified = await Promise.all(classifyTasks); + + return classified; + } + /** Updates the secret stored by this state. * @param configureState a callback that returns an updated decrypted * secret state. The callback receives the state's present value as its @@ -167,71 +167,30 @@ export class SecretState<Outer, Id, Plaintext extends object, Disclosed, Secret> configureState: (state: Outer, dependencies: TCombine) => Outer, options: StateUpdateOptions<Outer, TCombine> = null, ): Promise<Outer> { - // reactively grab the latest state from the caller. `zip` requires each - // observable has a value, so `combined$` provides a default if necessary. - const combined$ = options?.combineLatestWith ?? of(undefined); - const newState$ = zip(this.plaintext.state$, combined$).pipe( - concatMap(([currentState, combined]) => - this.prepareCryptoState( - currentState, - () => options?.shouldUpdate?.(currentState, combined) ?? true, - () => configureState(currentState, combined), - ), - ), - ); - - // update the backing store - let latestValue: Outer = null; - await this.encrypted.update((_, [, newStoredState]) => newStoredState, { - combineLatestWith: newState$, - shouldUpdate: (_, [shouldUpdate, , newState]) => { - // need to grab the latest value from the closure since the derived state - // could return its cached value, and this must be done in `shouldUpdate` - // because `configureState` may not run. - latestValue = newState; - return shouldUpdate; + // read the backing store + let latestClassified: ClassifiedFormat<Id, Disclosed>[]; + let latestCombined: TCombine; + await this.encryptedState.update((c) => c, { + shouldUpdate: (latest, combined) => { + latestClassified = latest; + latestCombined = combined; + return false; }, + combineLatestWith: options?.combineLatestWith, }); - return latestValue; - } - - private async prepareCryptoState( - currentState: Outer, - shouldUpdate: () => boolean, - configureState: () => Outer, - ): Promise<[boolean, ClassifiedFormat<Id, Disclosed>[], Outer]> { - // determine whether an update is necessary - if (!shouldUpdate()) { - return [false, undefined, currentState]; + // exit early if there's no update to apply + const latestDeclassified = await this.declassifyAll(latestClassified); + const shouldUpdate = options?.shouldUpdate?.(latestDeclassified, latestCombined) ?? true; + if (!shouldUpdate) { + return latestDeclassified; } - // calculate the update - const newState = configureState(); - if (newState === null || newState === undefined) { - return [true, newState as any, newState]; - } + // apply the update + const updatedDeclassified = configureState(latestDeclassified, latestCombined); + const updatedClassified = await this.classifyAll(updatedDeclassified); + await this.encryptedState.update(() => updatedClassified); - // convert the object to a list format so that all encrypt and decrypt - // operations are self-similar - const desconstructed = this.key.deconstruct(newState); - - // encrypt each value individually - const encryptTasks = desconstructed.map(async ([id, state]) => { - const classified = this.key.classifier.classify(state); - const encrypted = await this.encryptor.encrypt(classified.secret, this.encrypted.userId); - - // the deserializer in the plaintextState's `derive` configuration always runs, but - // `encryptedState` is not guaranteed to serialize the data, so it's necessary to - // round-trip it proactively. This will cause some duplicate work in those situations - // where the backing store does deserialize the data. - const serialized = JSON.parse( - JSON.stringify({ id, secret: encrypted, disclosed: classified.disclosed }), - ); - return serialized as ClassifiedFormat<Id, Disclosed>; - }); - const serializedState = await Promise.all(encryptTasks); - - return [true, serializedState, newState]; + return updatedDeclassified; } } diff --git a/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts b/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts index 2009c6f255..76539a0edf 100644 --- a/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts +++ b/libs/common/src/tools/generator/state/user-encryptor.abstraction.ts @@ -7,7 +7,7 @@ import { UserId } from "../../../types/guid"; * user-specific information. The specific kind of information is * determined by the classification strategy. */ -export abstract class UserEncryptor<Secret> { +export abstract class UserEncryptor { /** Protects secrets in `value` with a user-specific key. * @param secret the object to protect. This object is mutated during encryption. * @param userId identifies the user-specific information used to protect @@ -17,7 +17,7 @@ export abstract class UserEncryptor<Secret> { * properties. * @throws If `value` is `null` or `undefined`, the promise rejects with an error. */ - abstract encrypt(secret: Secret, userId: UserId): Promise<EncString>; + abstract encrypt<Secret>(secret: Jsonify<Secret>, userId: UserId): Promise<EncString>; /** Combines protected secrets and disclosed data into a type that can be * rehydrated into a domain object. @@ -30,5 +30,5 @@ export abstract class UserEncryptor<Secret> { * @throws If `secret` or `disclosed` is `null` or `undefined`, the promise * rejects with an error. */ - abstract decrypt(secret: EncString, userId: UserId): Promise<Jsonify<Secret>>; + abstract decrypt<Secret>(secret: EncString, userId: UserId): Promise<Jsonify<Secret>>; } diff --git a/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts b/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts index 9289086986..072f7bd8f3 100644 --- a/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts +++ b/libs/common/src/tools/generator/state/user-key-encryptor.spec.ts @@ -39,10 +39,10 @@ describe("UserKeyEncryptor", () => { it("should throw if value was not supplied", async () => { const encryptor = new UserKeyEncryptor(encryptService, keyService, dataPacker); - await expect(encryptor.encrypt(null, anyUserId)).rejects.toThrow( + await expect(encryptor.encrypt<Record<string, never>>(null, anyUserId)).rejects.toThrow( "secret cannot be null or undefined", ); - await expect(encryptor.encrypt(undefined, anyUserId)).rejects.toThrow( + await expect(encryptor.encrypt<Record<string, never>>(undefined, anyUserId)).rejects.toThrow( "secret cannot be null or undefined", ); }); @@ -50,10 +50,10 @@ describe("UserKeyEncryptor", () => { it("should throw if userId was not supplied", async () => { const encryptor = new UserKeyEncryptor(encryptService, keyService, dataPacker); - await expect(encryptor.encrypt({} as any, null)).rejects.toThrow( + await expect(encryptor.encrypt({}, null)).rejects.toThrow( "userId cannot be null or undefined", ); - await expect(encryptor.encrypt({} as any, undefined)).rejects.toThrow( + await expect(encryptor.encrypt({}, undefined)).rejects.toThrow( "userId cannot be null or undefined", ); }); diff --git a/libs/common/src/tools/generator/state/user-key-encryptor.ts b/libs/common/src/tools/generator/state/user-key-encryptor.ts index 22dbd41140..27724d820d 100644 --- a/libs/common/src/tools/generator/state/user-key-encryptor.ts +++ b/libs/common/src/tools/generator/state/user-key-encryptor.ts @@ -11,7 +11,7 @@ import { UserEncryptor } from "./user-encryptor.abstraction"; /** A classification strategy that protects a type's secrets by encrypting them * with a `UserKey` */ -export class UserKeyEncryptor<Secret> extends UserEncryptor<Secret> { +export class UserKeyEncryptor extends UserEncryptor { /** Instantiates the encryptor * @param encryptService protects properties of `Secret`. * @param keyService looks up the user key when protecting data. @@ -26,7 +26,7 @@ export class UserKeyEncryptor<Secret> extends UserEncryptor<Secret> { } /** {@link UserEncryptor.encrypt} */ - async encrypt(secret: Secret, userId: UserId): Promise<EncString> { + async encrypt<Secret>(secret: Jsonify<Secret>, userId: UserId): Promise<EncString> { this.assertHasValue("secret", secret); this.assertHasValue("userId", userId); @@ -42,7 +42,7 @@ export class UserKeyEncryptor<Secret> extends UserEncryptor<Secret> { } /** {@link UserEncryptor.decrypt} */ - async decrypt(secret: EncString, userId: UserId): Promise<Jsonify<Secret>> { + async decrypt<Secret>(secret: EncString, userId: UserId): Promise<Jsonify<Secret>> { this.assertHasValue("secret", secret); this.assertHasValue("userId", userId); diff --git a/libs/common/src/tools/generator/username/catchall-generator-options.ts b/libs/common/src/tools/generator/username/catchall-generator-options.ts index 7e9950ec45..bddf98f757 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-options.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-options.ts @@ -1,10 +1,21 @@ +import { RequestOptions } from "./options/forwarder-options"; +import { UsernameGenerationMode } from "./options/generator-options"; + /** Settings supported when generating an email subaddress */ export type CatchallGenerationOptions = { - type?: "random" | "website-name"; - domain?: string; -}; + /** selects the generation algorithm for the catchall email address. */ + catchallType?: UsernameGenerationMode; -/** The default options for email subaddress generation. */ -export const DefaultCatchallOptions: Partial<CatchallGenerationOptions> = Object.freeze({ - type: "random", + /** The domain part of the generated email address. + * @example If the domain is `domain.io` and the generated username + * is `jd`, then the generated email address will be `jd@mydomain.io` + */ + catchallDomain?: string; +} & RequestOptions; + +/** The default options for catchall address generation. */ +export const DefaultCatchallOptions: CatchallGenerationOptions = Object.freeze({ + catchallType: "random", + catchallDomain: "", + website: null, }); diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts index dafb55feba..52cfa00aaf 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.spec.ts @@ -1,4 +1,5 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -9,42 +10,31 @@ import { UserId } from "../../../types/guid"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { CATCHALL_SETTINGS } from "../key-definitions"; +import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options"; + import { CatchallGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock<Policy>({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("Email subaddress list generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new CatchallGeneratorStrategy(null, null); - const policy = mock<Policy>({ - type: PolicyType.DisableSend, - }); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new CatchallGeneratorStrategy(null, null); - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); - it("should map to the policy evaluator", () => { - const strategy = new CatchallGeneratorStrategy(null, null); - const policy = mock<Policy>({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, - }); - - const evaluator = strategy.evaluator(policy); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - expect(evaluator.policy).toMatchObject({}); - }); - - it("should map `null` to a default policy evaluator", () => { - const strategy = new CatchallGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }); + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); describe("durableState", () => { @@ -59,6 +49,16 @@ describe("Email subaddress list generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new CatchallGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultCatchallOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock<UsernameGenerationServiceAbstraction>(); @@ -82,16 +82,14 @@ describe("Email subaddress list generation strategy", () => { const legacy = mock<UsernameGenerationServiceAbstraction>(); const strategy = new CatchallGeneratorStrategy(legacy, null); const options = { - type: "website-name" as const, - domain: "example.com", - }; + catchallType: "website-name", + catchallDomain: "example.com", + website: "foo.com", + } as CatchallGenerationOptions; await strategy.generate(options); - expect(legacy.generateCatchall).toHaveBeenCalledWith({ - catchallType: "website-name" as const, - catchallDomain: "example.com", - }); + expect(legacy.generateCatchall).toHaveBeenCalledWith(options); }); }); }); diff --git a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts index aadca78b3b..5111b06e90 100644 --- a/libs/common/src/tools/generator/username/catchall-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/catchall-generator-strategy.ts @@ -1,14 +1,15 @@ +import { BehaviorSubject, map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { CATCHALL_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; -import { CatchallGenerationOptions } from "./catchall-generator-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; +import { CatchallGenerationOptions, DefaultCatchallOptions } from "./catchall-generator-options"; const ONE_MINUTE = 60 * 1000; @@ -29,6 +30,11 @@ export class CatchallGeneratorStrategy return this.stateProvider.getUser(id, CATCHALL_SETTINGS); } + /** {@link GeneratorStrategy.defaults$} */ + defaults$(userId: UserId) { + return new BehaviorSubject({ ...DefaultCatchallOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { // Uses password generator since there aren't policies @@ -41,25 +47,13 @@ export class CatchallGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator<CatchallGenerationOptions>(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator<CatchallGenerationOptions>(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator<CatchallGenerationOptions>())); } /** {@link GeneratorStrategy.generate} */ generate(options: CatchallGenerationOptions) { - return this.usernameService.generateCatchall({ - catchallDomain: options.domain, - catchallType: options.type, - }); + return this.usernameService.generateCatchall(options); } } diff --git a/libs/common/src/tools/generator/username/eff-username-generator-options.ts b/libs/common/src/tools/generator/username/eff-username-generator-options.ts index 868149c2fd..07890b3d55 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-options.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-options.ts @@ -1,11 +1,17 @@ -/** Settings supported when generating an ASCII username */ +import { RequestOptions } from "./options/forwarder-options"; + +/** Settings supported when generating a username using the EFF word list */ export type EffUsernameGenerationOptions = { + /** when true, the word is capitalized */ wordCapitalize?: boolean; + + /** when true, a random number is appended to the username */ wordIncludeNumber?: boolean; -}; +} & RequestOptions; /** The default options for EFF long word generation. */ -export const DefaultEffUsernameOptions: Partial<EffUsernameGenerationOptions> = Object.freeze({ +export const DefaultEffUsernameOptions: EffUsernameGenerationOptions = Object.freeze({ wordCapitalize: false, wordIncludeNumber: false, + website: null, }); diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts index 0fb5bf573c..9b0e4cc069 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.spec.ts @@ -1,4 +1,5 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -9,42 +10,31 @@ import { UserId } from "../../../types/guid"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { EFF_USERNAME_SETTINGS } from "../key-definitions"; +import { DefaultEffUsernameOptions } from "./eff-username-generator-options"; + import { EffUsernameGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock<Policy>({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("EFF long word list generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - const policy = mock<Policy>({ - type: PolicyType.DisableSend, - }); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new EffUsernameGeneratorStrategy(null, null); - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); - it("should map to the policy evaluator", () => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - const policy = mock<Policy>({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, - }); - - const evaluator = strategy.evaluator(policy); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - expect(evaluator.policy).toMatchObject({}); - }); - - it("should map `null` to a default policy evaluator", () => { - const strategy = new EffUsernameGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }); + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); describe("durableState", () => { @@ -59,6 +49,16 @@ describe("EFF long word list generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new EffUsernameGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultEffUsernameOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock<UsernameGenerationServiceAbstraction>(); @@ -84,6 +84,7 @@ describe("EFF long word list generation strategy", () => { const options = { wordCapitalize: false, wordIncludeNumber: false, + website: null as string, }; await strategy.generate(options); diff --git a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts index e0179895ae..1a4efdcb44 100644 --- a/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/eff-username-generator-strategy.ts @@ -1,14 +1,18 @@ +import { BehaviorSubject, map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { EFF_USERNAME_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; -import { EffUsernameGenerationOptions } from "./eff-username-generator-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; +import { + DefaultEffUsernameOptions, + EffUsernameGenerationOptions, +} from "./eff-username-generator-options"; const ONE_MINUTE = 60 * 1000; @@ -29,6 +33,11 @@ export class EffUsernameGeneratorStrategy return this.stateProvider.getUser(id, EFF_USERNAME_SETTINGS); } + /** {@link GeneratorStrategy.defaults$} */ + defaults$(userId: UserId) { + return new BehaviorSubject({ ...DefaultEffUsernameOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { // Uses password generator since there aren't policies @@ -41,18 +50,9 @@ export class EffUsernameGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator<EffUsernameGenerationOptions>(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator<EffUsernameGenerationOptions>())); } /** {@link GeneratorStrategy.generate} */ diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts index 96a7bca2b1..c2a606eae0 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.spec.ts @@ -1,6 +1,11 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { PolicyType } from "../../../admin-console/enums"; +// FIXME: use index.ts imports once policy abstractions and models +// implement ADR-0002 +import { Policy } from "../../../admin-console/models/domain/policy"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { StateProvider } from "../../../platform/state"; @@ -10,6 +15,7 @@ import { DUCK_DUCK_GO_FORWARDER } from "../key-definitions"; import { SecretState } from "../state/secret-state"; import { ForwarderGeneratorStrategy } from "./forwarder-generator-strategy"; +import { DefaultDuckDuckGoOptions } from "./forwarders/duck-duck-go"; import { ApiOptions } from "./options/forwarder-options"; class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> { @@ -25,10 +31,20 @@ class TestForwarder extends ForwarderGeneratorStrategy<ApiOptions> { // arbitrary. return DUCK_DUCK_GO_FORWARDER; } + + defaults$ = (userId: UserId) => { + return of(DefaultDuckDuckGoOptions); + }; } const SomeUser = "some user" as UserId; const AnotherUser = "another user" as UserId; +const SomePolicy = mock<Policy>({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("ForwarderGeneratorStrategy", () => { const encryptService = mock<EncryptService>(); @@ -63,11 +79,17 @@ describe("ForwarderGeneratorStrategy", () => { }); }); - it("evaluator returns the default policy evaluator", () => { - const strategy = new TestForwarder(null, null, null); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new TestForwarder(encryptService, keyService, stateProvider); - const result = strategy.evaluator(null); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); - expect(result).toBeInstanceOf(DefaultPolicyEvaluator); + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); }); diff --git a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts index b0717695e0..b4205b9fc9 100644 --- a/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/forwarder-generator-strategy.ts @@ -1,8 +1,9 @@ +import { Observable, map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; -import { KeyDefinition, SingleUserState, StateProvider } from "../../../platform/state"; +import { SingleUserState, StateProvider, UserKeyDefinition } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; @@ -55,6 +56,7 @@ export abstract class ForwarderGeneratorStrategy< const key = SecretKeyDefinition.value(this.key.stateDefinition, this.key.key, classifier, { deserializer: (d) => this.key.deserializer(d), cleanupDelayMs: this.key.cleanupDelayMs, + clearOn: this.key.clearOn, }); // the type parameter is explicit because type inference fails for `Omit<Options, "website">` @@ -78,11 +80,14 @@ export abstract class ForwarderGeneratorStrategy< return new UserKeyEncryptor(this.encryptService, this.keyService, packer); } - /** Determine where forwarder configuration is stored */ - protected abstract readonly key: KeyDefinition<Options>; + /** Gets the default options. */ + abstract defaults$: (userId: UserId) => Observable<Options>; - /** {@link GeneratorStrategy.evaluator} */ - evaluator = (_policy: Policy) => { - return new DefaultPolicyEvaluator<Options>(); + /** Determine where forwarder configuration is stored */ + protected abstract readonly key: UserKeyDefinition<Options>; + + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator = () => { + return pipe(map((_) => new DefaultPolicyEvaluator<Options>())); }; } diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts index c2428aefca..f42ca23c11 100644 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/addy-io.spec.ts @@ -2,12 +2,17 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { ADDY_IO_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { AddyIoForwarder } from "./addy-io"; +import { AddyIoForwarder, DefaultAddyIoOptions } from "./addy-io"; import { mockApiService, mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + describe("Addy.io Forwarder", () => { it("key returns the Addy IO forwarder key", () => { const forwarder = new AddyIoForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("Addy.io Forwarder", () => { expect(forwarder.key).toBe(ADDY_IO_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new AddyIoForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultAddyIoOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/addy-io.ts b/libs/common/src/tools/generator/username/forwarders/addy-io.ts index 2db69e2396..3e4960f7e7 100644 --- a/libs/common/src/tools/generator/username/forwarders/addy-io.ts +++ b/libs/common/src/tools/generator/username/forwarders/addy-io.ts @@ -1,13 +1,23 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { ADDY_IO_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { EmailDomainOptions, SelfHostedApiOptions } from "../options/forwarder-options"; +export const DefaultAddyIoOptions: SelfHostedApiOptions & EmailDomainOptions = Object.freeze({ + website: null, + baseUrl: "https://app.addy.io", + token: "", + domain: "", +}); + /** Generates a forwarding address for addy.io (formerly anon addy) */ export class AddyIoForwarder extends ForwarderGeneratorStrategy< SelfHostedApiOptions & EmailDomainOptions @@ -34,6 +44,11 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy< return ADDY_IO_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultAddyIoOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: SelfHostedApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { @@ -91,3 +106,10 @@ export class AddyIoForwarder extends ForwarderGeneratorStrategy< } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + baseUrl: "https://app.addy.io", + domain: "", + token: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts index 211eaead6d..b836ca2bef 100644 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts @@ -2,12 +2,17 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { DuckDuckGoForwarder } from "./duck-duck-go"; +import { DuckDuckGoForwarder, DefaultDuckDuckGoOptions } from "./duck-duck-go"; import { mockApiService, mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + describe("DuckDuckGo Forwarder", () => { it("key returns the Duck Duck Go forwarder key", () => { const forwarder = new DuckDuckGoForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("DuckDuckGo Forwarder", () => { expect(forwarder.key).toBe(DUCK_DUCK_GO_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new DuckDuckGoForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultDuckDuckGoOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts index daf4f7b444..9b5d93d742 100644 --- a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts @@ -1,13 +1,21 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { DUCK_DUCK_GO_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { ApiOptions } from "../options/forwarder-options"; +export const DefaultDuckDuckGoOptions: ApiOptions = Object.freeze({ + website: null, + token: "", +}); + /** Generates a forwarding address for DuckDuckGo */ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions> { /** Instantiates the forwarder @@ -32,6 +40,11 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions> return DUCK_DUCK_GO_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultDuckDuckGoOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions): Promise<string> => { if (!options.token || options.token === "") { @@ -68,3 +81,8 @@ export class DuckDuckGoForwarder extends ForwarderGeneratorStrategy<ApiOptions> } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + token: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts index bab2b93966..895f32f7ee 100644 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/fastmail.spec.ts @@ -2,13 +2,18 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; +import { UserId } from "../../../../types/guid"; import { FASTMAIL_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { FastmailForwarder } from "./fastmail"; +import { FastmailForwarder, DefaultFastmailOptions } from "./fastmail"; import { mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + type MockResponse = { status: number; body: any }; // fastmail calls nativeFetch first to resolve the accountId, @@ -52,6 +57,16 @@ describe("Fastmail Forwarder", () => { expect(forwarder.key).toBe(FASTMAIL_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new FastmailForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultFastmailOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(AccountIdSuccess, EmptyResponse); diff --git a/libs/common/src/tools/generator/username/forwarders/fastmail.ts b/libs/common/src/tools/generator/username/forwarders/fastmail.ts index b4e2b56695..9d62cd0039 100644 --- a/libs/common/src/tools/generator/username/forwarders/fastmail.ts +++ b/libs/common/src/tools/generator/username/forwarders/fastmail.ts @@ -1,13 +1,23 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { FASTMAIL_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { EmailPrefixOptions, ApiOptions } from "../options/forwarder-options"; +export const DefaultFastmailOptions: ApiOptions & EmailPrefixOptions = Object.freeze({ + website: null, + domain: "", + prefix: "", + token: "", +}); + /** Generates a forwarding address for Fastmail */ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & EmailPrefixOptions> { /** Instantiates the forwarder @@ -32,6 +42,11 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E return FASTMAIL_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultFastmailOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions & EmailPrefixOptions) => { if (!options.token || options.token === "") { @@ -141,3 +156,10 @@ export class FastmailForwarder extends ForwarderGeneratorStrategy<ApiOptions & E return null; } } + +export const DefaultOptions = Object.freeze({ + website: null, + domain: "", + prefix: "", + token: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts b/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts index 5ba8d3f2f1..7d712f7332 100644 --- a/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/firefox-relay.spec.ts @@ -2,12 +2,17 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { FirefoxRelayForwarder } from "./firefox-relay"; +import { FirefoxRelayForwarder, DefaultFirefoxRelayOptions } from "./firefox-relay"; import { mockApiService, mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + describe("Firefox Relay Forwarder", () => { it("key returns the Firefox Relay forwarder key", () => { const forwarder = new FirefoxRelayForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("Firefox Relay Forwarder", () => { expect(forwarder.key).toBe(FIREFOX_RELAY_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new FirefoxRelayForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultFirefoxRelayOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts b/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts index 1308852224..a4122c53f8 100644 --- a/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts +++ b/libs/common/src/tools/generator/username/forwarders/firefox-relay.ts @@ -1,13 +1,21 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { FIREFOX_RELAY_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { ApiOptions } from "../options/forwarder-options"; +export const DefaultFirefoxRelayOptions: ApiOptions = Object.freeze({ + website: null, + token: "", +}); + /** Generates a forwarding address for Firefox Relay */ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions> { /** Instantiates the forwarder @@ -32,6 +40,11 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions return FIREFOX_RELAY_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultFirefoxRelayOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions) => { if (!options.token || options.token === "") { @@ -75,3 +88,8 @@ export class FirefoxRelayForwarder extends ForwarderGeneratorStrategy<ApiOptions } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + token: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts index daf0f3d7f1..23c4bef64a 100644 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/forward-email.spec.ts @@ -2,12 +2,17 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; -import { ForwardEmailForwarder } from "./forward-email"; +import { ForwardEmailForwarder, DefaultForwardEmailOptions } from "./forward-email"; import { mockApiService, mockI18nService } from "./mocks.jest"; +const SomeUser = "some user" as UserId; + describe("ForwardEmail Forwarder", () => { it("key returns the Forward Email forwarder key", () => { const forwarder = new ForwardEmailForwarder(null, null, null, null, null); @@ -15,6 +20,16 @@ describe("ForwardEmail Forwarder", () => { expect(forwarder.key).toBe(FORWARD_EMAIL_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new ForwardEmailForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultForwardEmailOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/forward-email.ts b/libs/common/src/tools/generator/username/forwarders/forward-email.ts index eb6e3cd0c6..93f4680414 100644 --- a/libs/common/src/tools/generator/username/forwarders/forward-email.ts +++ b/libs/common/src/tools/generator/username/forwarders/forward-email.ts @@ -1,14 +1,23 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { Utils } from "../../../../platform/misc/utils"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { FORWARD_EMAIL_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { EmailDomainOptions, ApiOptions } from "../options/forwarder-options"; +export const DefaultForwardEmailOptions: ApiOptions & EmailDomainOptions = Object.freeze({ + website: null, + token: "", + domain: "", +}); + /** Generates a forwarding address for Forward Email */ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< ApiOptions & EmailDomainOptions @@ -35,6 +44,11 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< return FORWARD_EMAIL_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultForwardEmailOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: ApiOptions & EmailDomainOptions) => { if (!options.token || options.token === "") { @@ -96,3 +110,9 @@ export class ForwardEmailForwarder extends ForwarderGeneratorStrategy< } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + token: "", + domain: "", +}); diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts index 1120d49ce3..c53e783270 100644 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts +++ b/libs/common/src/tools/generator/username/forwarders/simple-login.spec.ts @@ -2,11 +2,16 @@ * include Request in test environment. * @jest-environment ../../../../shared/test.environment.ts */ +import { firstValueFrom } from "rxjs"; + +import { UserId } from "../../../../types/guid"; import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions"; import { Forwarders } from "../options/constants"; import { mockApiService, mockI18nService } from "./mocks.jest"; -import { SimpleLoginForwarder } from "./simple-login"; +import { SimpleLoginForwarder, DefaultSimpleLoginOptions } from "./simple-login"; + +const SomeUser = "some user" as UserId; describe("SimpleLogin Forwarder", () => { it("key returns the Simple Login forwarder key", () => { @@ -15,6 +20,16 @@ describe("SimpleLogin Forwarder", () => { expect(forwarder.key).toBe(SIMPLE_LOGIN_FORWARDER); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new SimpleLoginForwarder(null, null, null, null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultSimpleLoginOptions); + }); + }); + describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { const apiService = mockApiService(200, {}); diff --git a/libs/common/src/tools/generator/username/forwarders/simple-login.ts b/libs/common/src/tools/generator/username/forwarders/simple-login.ts index 33bd8e3d4e..d047fc42d1 100644 --- a/libs/common/src/tools/generator/username/forwarders/simple-login.ts +++ b/libs/common/src/tools/generator/username/forwarders/simple-login.ts @@ -1,13 +1,22 @@ +import { BehaviorSubject } from "rxjs"; + import { ApiService } from "../../../../abstractions/api.service"; import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../../platform/abstractions/i18n.service"; import { StateProvider } from "../../../../platform/state"; +import { UserId } from "../../../../types/guid"; import { SIMPLE_LOGIN_FORWARDER } from "../../key-definitions"; import { ForwarderGeneratorStrategy } from "../forwarder-generator-strategy"; import { Forwarders } from "../options/constants"; import { SelfHostedApiOptions } from "../options/forwarder-options"; +export const DefaultSimpleLoginOptions: SelfHostedApiOptions = Object.freeze({ + website: null, + baseUrl: "https://app.simplelogin.io", + token: "", +}); + /** Generates a forwarding address for Simple Login */ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedApiOptions> { /** Instantiates the forwarder @@ -32,6 +41,11 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedA return SIMPLE_LOGIN_FORWARDER; } + /** {@link ForwarderGeneratorStrategy.defaults$} */ + defaults$ = (userId: UserId) => { + return new BehaviorSubject({ ...DefaultSimpleLoginOptions }); + }; + /** {@link ForwarderGeneratorStrategy.generate} */ generate = async (options: SelfHostedApiOptions) => { if (!options.token || options.token === "") { @@ -80,3 +94,9 @@ export class SimpleLoginForwarder extends ForwarderGeneratorStrategy<SelfHostedA } }; } + +export const DefaultOptions = Object.freeze({ + website: null, + baseUrl: "https://app.simplelogin.io", + token: "", +}); diff --git a/libs/common/src/tools/generator/username/index.ts b/libs/common/src/tools/generator/username/index.ts index f9d5e3166e..7c5ec45f74 100644 --- a/libs/common/src/tools/generator/username/index.ts +++ b/libs/common/src/tools/generator/username/index.ts @@ -2,5 +2,5 @@ export { EffUsernameGeneratorStrategy } from "./eff-username-generator-strategy" export { CatchallGeneratorStrategy } from "./catchall-generator-strategy"; export { SubaddressGeneratorStrategy } from "./subaddress-generator-strategy"; export { UsernameGeneratorOptions } from "./username-generation-options"; -export { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; +export { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; export { UsernameGenerationService } from "./username-generation.service"; diff --git a/libs/common/src/tools/generator/username/options/constants.ts b/libs/common/src/tools/generator/username/options/constants.ts index 8f7013d48b..ab584effd5 100644 --- a/libs/common/src/tools/generator/username/options/constants.ts +++ b/libs/common/src/tools/generator/username/options/constants.ts @@ -1,5 +1,4 @@ import { ForwarderMetadata } from "./forwarder-options"; -import { UsernameGeneratorOptions } from "./generator-options"; /** Metadata about an email forwarding service. * @remarks This is used to populate the forwarder selection list @@ -48,71 +47,3 @@ export const Forwarders = Object.freeze({ validForSelfHosted: true, } as ForwarderMetadata), }); - -/** Padding values used to prevent leaking the length of the encrypted options. */ -export const SecretPadding = Object.freeze({ - /** The length to pad out encrypted members. This should be at least as long - * as the JSON content for the longest JSON payload being encrypted. - */ - length: 512, - - /** The character to use for padding. */ - character: "0", - - /** A regular expression for detecting invalid padding. When the character - * changes, this should be updated to include the new padding pattern. - */ - hasInvalidPadding: /[^0]/, -}); - -/** Default options for username generation. */ -// freeze all the things to prevent mutation -export const DefaultOptions: UsernameGeneratorOptions = Object.freeze({ - type: "word", - website: "", - word: Object.freeze({ - capitalize: true, - includeNumber: true, - }), - subaddress: Object.freeze({ - algorithm: "random", - email: "", - }), - catchall: Object.freeze({ - algorithm: "random", - domain: "", - }), - forwarders: Object.freeze({ - service: Forwarders.Fastmail.id, - fastMail: Object.freeze({ - website: null, - domain: "", - prefix: "", - token: "", - }), - addyIo: Object.freeze({ - website: null, - baseUrl: "https://app.addy.io", - domain: "", - token: "", - }), - forwardEmail: Object.freeze({ - website: null, - token: "", - domain: "", - }), - simpleLogin: Object.freeze({ - website: null, - baseUrl: "https://app.simplelogin.io", - token: "", - }), - duckDuckGo: Object.freeze({ - website: null, - token: "", - }), - firefoxRelay: Object.freeze({ - website: null, - token: "", - }), - }), -}); diff --git a/libs/common/src/tools/generator/username/options/generator-options.ts b/libs/common/src/tools/generator/username/options/generator-options.ts index 11fb045b62..3df5709ed3 100644 --- a/libs/common/src/tools/generator/username/options/generator-options.ts +++ b/libs/common/src/tools/generator/username/options/generator-options.ts @@ -1,98 +1,13 @@ -import { - ApiOptions, - EmailDomainOptions, - EmailPrefixOptions, - ForwarderId, - SelfHostedApiOptions, -} from "./forwarder-options"; - -/** Configuration for username generation algorithms. */ -export type AlgorithmOptions = { - /** selects the generation algorithm for the username. - * "random" generates a random string. - * "website-name" generates a username based on the website's name. - */ - algorithm: "random" | "website-name"; -}; - -/** Identifies encrypted options that could have leaked from the configuration. */ -export type MaybeLeakedOptions = { - /** When true, encrypted options were previously stored as plaintext. - * @remarks This is used to alert the user that the token should be - * regenerated. If a token has always been stored encrypted, - * this should be omitted. - */ - wasPlainText?: true; -}; - -/** Options for generating a username. - * @remarks This type includes all fields so that the generator - * remembers the user's configuration for each type of username - * and forwarder. +/** ways you can generate usernames + * "word" generates a username from the eff word list + * "subaddress" creates a subaddress of an email. + * "catchall" uses a domain's catchall address + * "forwarded" uses an email forwarding service */ -export type UsernameGeneratorOptions = { - /** selects the property group used for username generation */ - type?: "word" | "subaddress" | "catchall" | "forwarded"; +export type UsernameGeneratorType = "word" | "subaddress" | "catchall" | "forwarded"; - /** When generating a forwarding address for a vault item, this should contain - * the domain the vault item supplies to the generator. - * @example If the user is creating a vault item for `https://www.domain.io/login`, - * then this should be `www.domain.io`. - */ - website?: string; - - /** When true, the username generator saves options immediately - * after they're loaded. Otherwise this option should not be defined. - * */ - saveOnLoad?: true; - - /* Configures generation of a username from the EFF word list */ - word: { - /** when true, the word is capitalized */ - capitalize?: boolean; - - /** when true, a random number is appended to the username */ - includeNumber?: boolean; - }; - - /** Configures generation of an email subaddress. - * @remarks The subaddress is the part following the `+`. - * For example, if the email address is `jd+xyz@domain.io`, - * the subaddress is `xyz`. - */ - subaddress: AlgorithmOptions & { - /** the email address the subaddress is applied to. */ - email?: string; - }; - - /** Configures generation for a domain catch-all address. - */ - catchall: AlgorithmOptions & EmailDomainOptions; - - /** Configures generation for an email forwarding service address. - */ - forwarders: { - /** The service to use for email forwarding. - * @remarks This determines which forwarder-specific options to use. - */ - service?: ForwarderId; - - /** {@link Forwarders.AddyIo} */ - addyIo: SelfHostedApiOptions & EmailDomainOptions & MaybeLeakedOptions; - - /** {@link Forwarders.DuckDuckGo} */ - duckDuckGo: ApiOptions & MaybeLeakedOptions; - - /** {@link Forwarders.FastMail} */ - fastMail: ApiOptions & EmailPrefixOptions & MaybeLeakedOptions; - - /** {@link Forwarders.FireFoxRelay} */ - firefoxRelay: ApiOptions & MaybeLeakedOptions; - - /** {@link Forwarders.ForwardEmail} */ - forwardEmail: ApiOptions & EmailDomainOptions & MaybeLeakedOptions; - - /** {@link forwarders.SimpleLogin} */ - simpleLogin: SelfHostedApiOptions & MaybeLeakedOptions; - }; -}; +/** Several username generators support two generation modes + * "random" selects one or more random words from the EFF word list + * "website-name" includes the domain in the generated username + */ +export type UsernameGenerationMode = "random" | "website-name"; diff --git a/libs/common/src/tools/generator/username/options/index.ts b/libs/common/src/tools/generator/username/options/index.ts index b76e5eaf51..b2d4066c87 100644 --- a/libs/common/src/tools/generator/username/options/index.ts +++ b/libs/common/src/tools/generator/username/options/index.ts @@ -1,3 +1 @@ -export { UsernameGeneratorOptions } from "./generator-options"; -export { DefaultOptions } from "./constants"; export { ForwarderId, ForwarderMetadata } from "./forwarder-options"; diff --git a/libs/common/src/tools/generator/username/options/utilities.spec.ts b/libs/common/src/tools/generator/username/options/utilities.spec.ts deleted file mode 100644 index 7ab1d9dcfd..0000000000 --- a/libs/common/src/tools/generator/username/options/utilities.spec.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * include structuredClone in test environment. - * @jest-environment ../../../../shared/test.environment.ts - */ -import { DefaultOptions, Forwarders } from "./constants"; -import { UsernameGeneratorOptions } from "./generator-options"; -import { getForwarderOptions, falsyDefault, forAllForwarders } from "./utilities"; - -const TestOptions: UsernameGeneratorOptions = { - type: "word", - website: "example.com", - word: { - capitalize: true, - includeNumber: true, - }, - subaddress: { - algorithm: "random", - email: "foo@example.com", - }, - catchall: { - algorithm: "random", - domain: "example.com", - }, - forwarders: { - service: Forwarders.Fastmail.id, - fastMail: { - website: null, - domain: "httpbin.com", - prefix: "foo", - token: "some-token", - }, - addyIo: { - website: null, - baseUrl: "https://app.addy.io", - domain: "example.com", - token: "some-token", - }, - forwardEmail: { - website: null, - token: "some-token", - domain: "example.com", - }, - simpleLogin: { - website: null, - baseUrl: "https://app.simplelogin.io", - token: "some-token", - }, - duckDuckGo: { - website: null, - token: "some-token", - }, - firefoxRelay: { - website: null, - token: "some-token", - }, - }, -}; - -describe("Username Generation Options", () => { - describe("forAllForwarders", () => { - it("runs the function on every forwarder.", () => { - const result = forAllForwarders(TestOptions, (_, id) => id); - expect(result).toEqual([ - "anonaddy", - "duckduckgo", - "fastmail", - "firefoxrelay", - "forwardemail", - "simplelogin", - ]); - }); - }); - - describe("getForwarderOptions", () => { - it("should return null for unsupported services", () => { - expect(getForwarderOptions("unsupported", DefaultOptions)).toBeNull(); - }); - - let options: UsernameGeneratorOptions = null; - beforeEach(() => { - options = structuredClone(TestOptions); - }); - - it.each([ - [TestOptions.forwarders.addyIo, "anonaddy"], - [TestOptions.forwarders.duckDuckGo, "duckduckgo"], - [TestOptions.forwarders.fastMail, "fastmail"], - [TestOptions.forwarders.firefoxRelay, "firefoxrelay"], - [TestOptions.forwarders.forwardEmail, "forwardemail"], - [TestOptions.forwarders.simpleLogin, "simplelogin"], - ])("should return an %s for %p", (forwarderOptions, service) => { - const forwarder = getForwarderOptions(service, options); - expect(forwarder).toEqual(forwarderOptions); - }); - - it("should return a reference to the forwarder", () => { - const forwarder = getForwarderOptions("anonaddy", options); - expect(forwarder).toBe(options.forwarders.addyIo); - }); - }); - - describe("falsyDefault", () => { - it("should not modify values with truthy items", () => { - const input = { - a: "a", - b: 1, - d: [1], - }; - - const output = falsyDefault(input, { - a: "b", - b: 2, - d: [2], - }); - - expect(output).toEqual(input); - }); - - it("should modify values with falsy items", () => { - const input = { - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }; - - const output = falsyDefault(input, { - a: "a", - b: 1, - c: true, - d: [1], - e: [1], - f: "a", - g: "a", - }); - - expect(output).toEqual({ - a: "a", - b: 1, - c: true, - d: [1], - e: [1], - f: "a", - g: "a", - }); - }); - - it("should traverse nested objects", () => { - const input = { - a: { - b: { - c: "", - }, - }, - }; - - const output = falsyDefault(input, { - a: { - b: { - c: "c", - }, - }, - }); - - expect(output).toEqual({ - a: { - b: { - c: "c", - }, - }, - }); - }); - - it("should add missing defaults", () => { - const input = {}; - - const output = falsyDefault(input, { - a: "a", - b: [1], - c: {}, - d: { e: 1 }, - }); - - expect(output).toEqual({ - a: "a", - b: [1], - c: {}, - d: { e: 1 }, - }); - }); - - it("should ignore missing defaults", () => { - const input = { - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }; - - const output = falsyDefault(input, {}); - - expect(output).toEqual({ - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }); - }); - - it.each([[null], [undefined]])("should ignore %p defaults", (defaults) => { - const input = { - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }; - - const output = falsyDefault(input, defaults); - - expect(output).toEqual({ - a: "", - b: 0, - c: false, - d: [] as number[], - e: [0] as number[], - f: null as string, - g: undefined as string, - }); - }); - }); -}); diff --git a/libs/common/src/tools/generator/username/options/utilities.ts b/libs/common/src/tools/generator/username/options/utilities.ts deleted file mode 100644 index ba0c6c291f..0000000000 --- a/libs/common/src/tools/generator/username/options/utilities.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { DefaultOptions, Forwarders } from "./constants"; -import { ApiOptions, ForwarderId } from "./forwarder-options"; -import { MaybeLeakedOptions, UsernameGeneratorOptions } from "./generator-options"; - -/** runs the callback on each forwarder configuration */ -export function forAllForwarders<T>( - options: UsernameGeneratorOptions, - callback: (options: ApiOptions, id: ForwarderId) => T, -) { - const results = []; - for (const forwarder of Object.values(Forwarders).map((f) => f.id)) { - const forwarderOptions = getForwarderOptions(forwarder, options); - if (forwarderOptions) { - results.push(callback(forwarderOptions, forwarder)); - } - } - return results; -} - -/** Gets the options for the specified forwarding service with defaults applied. - * This method mutates `options`. - * @param service Identifies the service whose options should be loaded. - * @param options The options to load from. - * @returns A reference to the options for the specified service. - */ -export function getForwarderOptions( - service: string, - options: UsernameGeneratorOptions, -): ApiOptions & MaybeLeakedOptions { - if (service === Forwarders.AddyIo.id) { - return falsyDefault(options.forwarders.addyIo, DefaultOptions.forwarders.addyIo); - } else if (service === Forwarders.DuckDuckGo.id) { - return falsyDefault(options.forwarders.duckDuckGo, DefaultOptions.forwarders.duckDuckGo); - } else if (service === Forwarders.Fastmail.id) { - return falsyDefault(options.forwarders.fastMail, DefaultOptions.forwarders.fastMail); - } else if (service === Forwarders.FirefoxRelay.id) { - return falsyDefault(options.forwarders.firefoxRelay, DefaultOptions.forwarders.firefoxRelay); - } else if (service === Forwarders.ForwardEmail.id) { - return falsyDefault(options.forwarders.forwardEmail, DefaultOptions.forwarders.forwardEmail); - } else if (service === Forwarders.SimpleLogin.id) { - return falsyDefault(options.forwarders.simpleLogin, DefaultOptions.forwarders.simpleLogin); - } else { - return null; - } -} - -/** - * Recursively applies default values from `defaults` to falsy or - * missing properties in `value`. - * - * @remarks This method is not aware of the - * object's prototype or metadata, such as readonly or frozen fields. - * It should only be used on plain objects. - * - * @param value - The value to fill in. This parameter is mutated. - * @param defaults - The default values to use. - * @returns the mutated `value`. - */ -export function falsyDefault<T>(value: T, defaults: Partial<T>): T { - // iterate keys in defaults because `value` may be missing keys - for (const key in defaults) { - if (defaults[key] instanceof Object) { - // `any` type is required because typescript can't predict the type of `value[key]`. - const target: any = value[key] || (defaults[key] instanceof Array ? [] : {}); - value[key] = falsyDefault(target, defaults[key]); - } else if (!value[key]) { - value[key] = defaults[key]; - } - } - - return value; -} diff --git a/libs/common/src/tools/generator/username/subaddress-generator-options.ts b/libs/common/src/tools/generator/username/subaddress-generator-options.ts index a43b8798ed..dc38b2a6ea 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-options.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-options.ts @@ -1,10 +1,18 @@ +import { RequestOptions } from "./options/forwarder-options"; +import { UsernameGenerationMode } from "./options/generator-options"; + /** Settings supported when generating an email subaddress */ export type SubaddressGenerationOptions = { - type?: "random" | "website-name"; - email?: string; -}; + /** selects the generation algorithm for the catchall email address. */ + subaddressType?: UsernameGenerationMode; + + /** the email address the subaddress is applied to. */ + subaddressEmail?: string; +} & RequestOptions; /** The default options for email subaddress generation. */ -export const DefaultSubaddressOptions: Partial<SubaddressGenerationOptions> = Object.freeze({ - type: "random", +export const DefaultSubaddressOptions: SubaddressGenerationOptions = Object.freeze({ + subaddressType: "random", + subaddressEmail: "", + website: null, }); diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts index 105edd6b4d..827bc7aed0 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.spec.ts @@ -1,4 +1,5 @@ import { mock } from "jest-mock-extended"; +import { of, firstValueFrom } from "rxjs"; import { PolicyType } from "../../../admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models @@ -9,42 +10,34 @@ import { UserId } from "../../../types/guid"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { SUBADDRESS_SETTINGS } from "../key-definitions"; +import { + DefaultSubaddressOptions, + SubaddressGenerationOptions, +} from "./subaddress-generator-options"; + import { SubaddressGeneratorStrategy, UsernameGenerationServiceAbstraction } from "."; const SomeUser = "some user" as UserId; +const SomePolicy = mock<Policy>({ + type: PolicyType.PasswordGenerator, + data: { + minLength: 10, + }, +}); describe("Email subaddress list generation strategy", () => { - describe("evaluator()", () => { - it("should throw if the policy type is incorrect", () => { - const strategy = new SubaddressGeneratorStrategy(null, null); - const policy = mock<Policy>({ - type: PolicyType.DisableSend, - }); + describe("toEvaluator()", () => { + it.each([[[]], [null], [undefined], [[SomePolicy]], [[SomePolicy, SomePolicy]]])( + "should map any input (= %p) to the default policy evaluator", + async (policies) => { + const strategy = new SubaddressGeneratorStrategy(null, null); - expect(() => strategy.evaluator(policy)).toThrow(new RegExp("Mismatched policy type\\. .+")); - }); + const evaluator$ = of(policies).pipe(strategy.toEvaluator()); + const evaluator = await firstValueFrom(evaluator$); - it("should map to the policy evaluator", () => { - const strategy = new SubaddressGeneratorStrategy(null, null); - const policy = mock<Policy>({ - type: PolicyType.PasswordGenerator, - data: { - minLength: 10, - }, - }); - - const evaluator = strategy.evaluator(policy); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - expect(evaluator.policy).toMatchObject({}); - }); - - it("should map `null` to a default policy evaluator", () => { - const strategy = new SubaddressGeneratorStrategy(null, null); - const evaluator = strategy.evaluator(null); - - expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); - }); + expect(evaluator).toBeInstanceOf(DefaultPolicyEvaluator); + }, + ); }); describe("durableState", () => { @@ -59,6 +52,16 @@ describe("Email subaddress list generation strategy", () => { }); }); + describe("defaults$", () => { + it("should return the default subaddress options", async () => { + const strategy = new SubaddressGeneratorStrategy(null, null); + + const result = await firstValueFrom(strategy.defaults$(SomeUser)); + + expect(result).toEqual(DefaultSubaddressOptions); + }); + }); + describe("cache_ms", () => { it("should be a positive non-zero number", () => { const legacy = mock<UsernameGenerationServiceAbstraction>(); @@ -82,16 +85,14 @@ describe("Email subaddress list generation strategy", () => { const legacy = mock<UsernameGenerationServiceAbstraction>(); const strategy = new SubaddressGeneratorStrategy(legacy, null); const options = { - type: "website-name" as const, - email: "someone@example.com", - }; + subaddressType: "website-name", + subaddressEmail: "someone@example.com", + website: "foo.com", + } as SubaddressGenerationOptions; await strategy.generate(options); - expect(legacy.generateSubaddress).toHaveBeenCalledWith({ - subaddressType: "website-name" as const, - subaddressEmail: "someone@example.com", - }); + expect(legacy.generateSubaddress).toHaveBeenCalledWith(options); }); }); }); diff --git a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts index 1aba473476..818741f8a9 100644 --- a/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts +++ b/libs/common/src/tools/generator/username/subaddress-generator-strategy.ts @@ -1,18 +1,26 @@ +import { BehaviorSubject, map, pipe } from "rxjs"; + import { PolicyType } from "../../../admin-console/enums"; -import { Policy } from "../../../admin-console/models/domain/policy"; import { StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { GeneratorStrategy } from "../abstractions"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { DefaultPolicyEvaluator } from "../default-policy-evaluator"; import { SUBADDRESS_SETTINGS } from "../key-definitions"; import { NoPolicy } from "../no-policy"; -import { SubaddressGenerationOptions } from "./subaddress-generator-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; +import { + DefaultSubaddressOptions, + SubaddressGenerationOptions, +} from "./subaddress-generator-options"; const ONE_MINUTE = 60 * 1000; -/** Strategy for creating an email subaddress */ +/** Strategy for creating an email subaddress + * @remarks The subaddress is the part following the `+`. + * For example, if the email address is `jd+xyz@domain.io`, + * the subaddress is `xyz`. + */ export class SubaddressGeneratorStrategy implements GeneratorStrategy<SubaddressGenerationOptions, NoPolicy> { @@ -29,6 +37,11 @@ export class SubaddressGeneratorStrategy return this.stateProvider.getUser(id, SUBADDRESS_SETTINGS); } + /** {@link GeneratorStrategy.defaults$} */ + defaults$(userId: UserId) { + return new BehaviorSubject({ ...DefaultSubaddressOptions }).asObservable(); + } + /** {@link GeneratorStrategy.policy} */ get policy() { // Uses password generator since there aren't policies @@ -41,25 +54,13 @@ export class SubaddressGeneratorStrategy return ONE_MINUTE; } - /** {@link GeneratorStrategy.evaluator} */ - evaluator(policy: Policy) { - if (!policy) { - return new DefaultPolicyEvaluator<SubaddressGenerationOptions>(); - } - - if (policy.type !== this.policy) { - const details = `Expected: ${this.policy}. Received: ${policy.type}`; - throw Error("Mismatched policy type. " + details); - } - - return new DefaultPolicyEvaluator<SubaddressGenerationOptions>(); + /** {@link GeneratorStrategy.toEvaluator} */ + toEvaluator() { + return pipe(map((_) => new DefaultPolicyEvaluator<SubaddressGenerationOptions>())); } /** {@link GeneratorStrategy.generate} */ generate(options: SubaddressGenerationOptions) { - return this.usernameService.generateSubaddress({ - subaddressEmail: options.email, - subaddressType: options.type, - }); + return this.usernameService.generateSubaddress(options); } } diff --git a/libs/common/src/tools/generator/username/username-generation-options.ts b/libs/common/src/tools/generator/username/username-generation-options.ts index 2cb1e8dfd6..b52b4c0848 100644 --- a/libs/common/src/tools/generator/username/username-generation-options.ts +++ b/libs/common/src/tools/generator/username/username-generation-options.ts @@ -1,21 +1,23 @@ +import { CatchallGenerationOptions } from "./catchall-generator-options"; import { EffUsernameGenerationOptions } from "./eff-username-generator-options"; +import { ForwarderId, RequestOptions } from "./options/forwarder-options"; +import { UsernameGeneratorType } from "./options/generator-options"; +import { SubaddressGenerationOptions } from "./subaddress-generator-options"; -export type UsernameGeneratorOptions = EffUsernameGenerationOptions & { - type?: "word" | "subaddress" | "catchall" | "forwarded"; - subaddressType?: "random" | "website-name"; - subaddressEmail?: string; - catchallType?: "random" | "website-name"; - catchallDomain?: string; - website?: string; - forwardedService?: string; - forwardedAnonAddyApiToken?: string; - forwardedAnonAddyDomain?: string; - forwardedAnonAddyBaseUrl?: string; - forwardedDuckDuckGoToken?: string; - forwardedFirefoxApiToken?: string; - forwardedFastmailApiToken?: string; - forwardedForwardEmailApiToken?: string; - forwardedForwardEmailDomain?: string; - forwardedSimpleLoginApiKey?: string; - forwardedSimpleLoginBaseUrl?: string; -}; +export type UsernameGeneratorOptions = EffUsernameGenerationOptions & + SubaddressGenerationOptions & + CatchallGenerationOptions & + RequestOptions & { + type?: UsernameGeneratorType; + forwardedService?: ForwarderId | ""; + forwardedAnonAddyApiToken?: string; + forwardedAnonAddyDomain?: string; + forwardedAnonAddyBaseUrl?: string; + forwardedDuckDuckGoToken?: string; + forwardedFirefoxApiToken?: string; + forwardedFastmailApiToken?: string; + forwardedForwardEmailApiToken?: string; + forwardedForwardEmailDomain?: string; + forwardedSimpleLoginApiKey?: string; + forwardedSimpleLoginBaseUrl?: string; + }; diff --git a/libs/common/src/tools/generator/username/username-generation.service.ts b/libs/common/src/tools/generator/username/username-generation.service.ts index 245e7575b7..1ee642da5e 100644 --- a/libs/common/src/tools/generator/username/username-generation.service.ts +++ b/libs/common/src/tools/generator/username/username-generation.service.ts @@ -2,6 +2,7 @@ import { ApiService } from "../../../abstractions/api.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { EFFLongWordList } from "../../../platform/misc/wordlist"; +import { UsernameGenerationServiceAbstraction } from "../abstractions/username-generation.service.abstraction"; import { AnonAddyForwarder, @@ -14,10 +15,10 @@ import { SimpleLoginForwarder, } from "./email-forwarders"; import { UsernameGeneratorOptions } from "./username-generation-options"; -import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; const DefaultOptions: UsernameGeneratorOptions = { type: "word", + website: null, wordCapitalize: true, wordIncludeNumber: true, subaddressType: "random", diff --git a/libs/common/src/tools/send/services/key-definitions.spec.ts b/libs/common/src/tools/send/services/key-definitions.spec.ts new file mode 100644 index 0000000000..9916237349 --- /dev/null +++ b/libs/common/src/tools/send/services/key-definitions.spec.ts @@ -0,0 +1,21 @@ +import { SEND_USER_ENCRYPTED, SEND_USER_DECRYPTED } from "./key-definitions"; +import { testSendData, testSendViewData } from "./test-data/send-tests.data"; + +describe("Key definitions", () => { + describe("SEND_USER_ENCRYPTED", () => { + it("should pass through deserialization", () => { + const result = SEND_USER_ENCRYPTED.deserializer( + JSON.parse(JSON.stringify(testSendData("1", "Test Send Data"))), + ); + expect(result).toEqual(testSendData("1", "Test Send Data")); + }); + }); + + describe("SEND_USER_DECRYPTED", () => { + it("should pass through deserialization", () => { + const sendViews = [testSendViewData("1", "Test Send View")]; + const result = SEND_USER_DECRYPTED.deserializer(JSON.parse(JSON.stringify(sendViews))); + expect(result).toEqual(sendViews); + }); + }); +}); diff --git a/libs/common/src/tools/send/services/key-definitions.ts b/libs/common/src/tools/send/services/key-definitions.ts new file mode 100644 index 0000000000..f1a6b3d6c6 --- /dev/null +++ b/libs/common/src/tools/send/services/key-definitions.ts @@ -0,0 +1,23 @@ +import { SEND_DISK, SEND_MEMORY, UserKeyDefinition } from "../../../platform/state"; +import { SendData } from "../models/data/send.data"; +import { SendView } from "../models/view/send.view"; + +/** Encrypted send state stored on disk */ +export const SEND_USER_ENCRYPTED = UserKeyDefinition.record<SendData>( + SEND_DISK, + "sendUserEncrypted", + { + deserializer: (obj: SendData) => obj, + clearOn: ["logout"], + }, +); + +/** Decrypted send state stored in memory */ +export const SEND_USER_DECRYPTED = new UserKeyDefinition<SendView[]>( + SEND_MEMORY, + "sendUserDecrypted", + { + deserializer: (obj) => obj, + clearOn: ["lock"], + }, +); diff --git a/libs/common/src/tools/send/services/send-state.provider.abstraction.ts b/libs/common/src/tools/send/services/send-state.provider.abstraction.ts new file mode 100644 index 0000000000..7a35506b56 --- /dev/null +++ b/libs/common/src/tools/send/services/send-state.provider.abstraction.ts @@ -0,0 +1,17 @@ +import { Observable } from "rxjs"; + +import { SendData } from "../models/data/send.data"; +import { SendView } from "../models/view/send.view"; + +export abstract class SendStateProvider { + encryptedState$: Observable<Record<string, SendData>>; + decryptedState$: Observable<SendView[]>; + + getEncryptedSends: () => Promise<{ [id: string]: SendData }>; + + setEncryptedSends: (value: { [id: string]: SendData }) => Promise<void>; + + getDecryptedSends: () => Promise<SendView[]>; + + setDecryptedSends: (value: SendView[]) => Promise<void>; +} diff --git a/libs/common/src/tools/send/services/send-state.provider.spec.ts b/libs/common/src/tools/send/services/send-state.provider.spec.ts new file mode 100644 index 0000000000..069e0d8069 --- /dev/null +++ b/libs/common/src/tools/send/services/send-state.provider.spec.ts @@ -0,0 +1,48 @@ +import { + FakeAccountService, + FakeStateProvider, + awaitAsync, + mockAccountServiceWith, +} from "../../../../spec"; +import { Utils } from "../../../platform/misc/utils"; +import { UserId } from "../../../types/guid"; + +import { SendStateProvider } from "./send-state.provider"; +import { testSendData, testSendViewData } from "./test-data/send-tests.data"; + +describe("Send State Provider", () => { + let stateProvider: FakeStateProvider; + let accountService: FakeAccountService; + let sendStateProvider: SendStateProvider; + + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + + sendStateProvider = new SendStateProvider(stateProvider); + }); + + describe("Encrypted Sends", () => { + it("should return SendData", async () => { + const sendData = { "1": testSendData("1", "Test Send Data") }; + await sendStateProvider.setEncryptedSends(sendData); + await awaitAsync(); + + const actual = await sendStateProvider.getEncryptedSends(); + expect(actual).toStrictEqual(sendData); + }); + }); + + describe("Decrypted Sends", () => { + it("should return SendView", async () => { + const state = [testSendViewData("1", "Test")]; + await sendStateProvider.setDecryptedSends(state); + await awaitAsync(); + + const actual = await sendStateProvider.getDecryptedSends(); + expect(actual).toStrictEqual(state); + }); + }); +}); diff --git a/libs/common/src/tools/send/services/send-state.provider.ts b/libs/common/src/tools/send/services/send-state.provider.ts new file mode 100644 index 0000000000..1e9397b7a9 --- /dev/null +++ b/libs/common/src/tools/send/services/send-state.provider.ts @@ -0,0 +1,47 @@ +import { Observable, firstValueFrom } from "rxjs"; + +import { ActiveUserState, StateProvider } from "../../../platform/state"; +import { SendData } from "../models/data/send.data"; +import { SendView } from "../models/view/send.view"; + +import { SEND_USER_DECRYPTED, SEND_USER_ENCRYPTED } from "./key-definitions"; +import { SendStateProvider as SendStateProviderAbstraction } from "./send-state.provider.abstraction"; + +/** State provider for sends */ +export class SendStateProvider implements SendStateProviderAbstraction { + /** Observable for the encrypted sends for an active user */ + encryptedState$: Observable<Record<string, SendData>>; + /** Observable with the decrypted sends for an active user */ + decryptedState$: Observable<SendView[]>; + + private activeUserEncryptedState: ActiveUserState<Record<string, SendData>>; + private activeUserDecryptedState: ActiveUserState<SendView[]>; + + constructor(protected stateProvider: StateProvider) { + this.activeUserEncryptedState = this.stateProvider.getActive(SEND_USER_ENCRYPTED); + this.encryptedState$ = this.activeUserEncryptedState.state$; + + this.activeUserDecryptedState = this.stateProvider.getActive(SEND_USER_DECRYPTED); + this.decryptedState$ = this.activeUserDecryptedState.state$; + } + + /** Gets the encrypted sends from state for an active user */ + async getEncryptedSends(): Promise<{ [id: string]: SendData }> { + return await firstValueFrom(this.encryptedState$); + } + + /** Sets the encrypted send state for an active user */ + async setEncryptedSends(value: { [id: string]: SendData }): Promise<void> { + await this.activeUserEncryptedState.update(() => value); + } + + /** Gets the decrypted sends from state for the active user */ + async getDecryptedSends(): Promise<SendView[]> { + return await firstValueFrom(this.decryptedState$); + } + + /** Sets the decrypted send state for an active user */ + async setDecryptedSends(value: SendView[]): Promise<void> { + await this.activeUserDecryptedState.update(() => value); + } +} diff --git a/libs/common/src/tools/send/services/send.service.abstraction.ts b/libs/common/src/tools/send/services/send.service.abstraction.ts index 45f623537d..e9f9387169 100644 --- a/libs/common/src/tools/send/services/send.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send.service.abstraction.ts @@ -18,10 +18,6 @@ export abstract class SendService { password: string, key?: SymmetricCryptoKey, ) => Promise<[Send, EncArrayBuffer]>; - /** - * @deprecated Do not call this, use the get$ method - */ - get: (id: string) => Send; /** * Provides a send for a determined id * updates after a change occurs to the send that matches the id @@ -53,6 +49,5 @@ export abstract class SendService { export abstract class InternalSendService extends SendService { upsert: (send: SendData | SendData[]) => Promise<any>; replace: (sends: { [id: string]: SendData }) => Promise<void>; - clear: (userId: string) => Promise<any>; delete: (id: string | string[]) => Promise<any>; } diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 568bd70d52..41183c42af 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -1,14 +1,22 @@ -import { any, mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { mock } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; +import { + FakeAccountService, + FakeActiveUserState, + FakeStateProvider, + awaitAsync, + mockAccountServiceWith, +} from "../../../../spec"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; -import { StateService } from "../../../platform/abstractions/state.service"; +import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; +import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { SendType } from "../enums/send-type"; import { SendFileApi } from "../models/api/send-file.api"; @@ -16,10 +24,17 @@ import { SendTextApi } from "../models/api/send-text.api"; import { SendFileData } from "../models/data/send-file.data"; import { SendTextData } from "../models/data/send-text.data"; import { SendData } from "../models/data/send.data"; -import { Send } from "../models/domain/send"; import { SendView } from "../models/view/send.view"; +import { SEND_USER_DECRYPTED, SEND_USER_ENCRYPTED } from "./key-definitions"; +import { SendStateProvider } from "./send-state.provider"; import { SendService } from "./send.service"; +import { + createSendData, + testSend, + testSendData, + testSendViewData, +} from "./test-data/send-tests.data"; describe("SendService", () => { const cryptoService = mock<CryptoService>(); @@ -27,56 +42,52 @@ describe("SendService", () => { const keyGenerationService = mock<KeyGenerationService>(); const encryptService = mock<EncryptService>(); + let sendStateProvider: SendStateProvider; let sendService: SendService; - let stateService: MockProxy<StateService>; - let activeAccount: BehaviorSubject<string>; - let activeAccountUnlocked: BehaviorSubject<boolean>; + let stateProvider: FakeStateProvider; + let encryptedState: FakeActiveUserState<Record<string, SendData>>; + let decryptedState: FakeActiveUserState<SendView[]>; + + const mockUserId = Utils.newGuid() as UserId; + let accountService: FakeAccountService; beforeEach(() => { - activeAccount = new BehaviorSubject("123"); - activeAccountUnlocked = new BehaviorSubject(true); + accountService = mockAccountServiceWith(mockUserId); + stateProvider = new FakeStateProvider(accountService); + sendStateProvider = new SendStateProvider(stateProvider); - stateService = mock<StateService>(); - stateService.activeAccount$ = activeAccount; - stateService.activeAccountUnlocked$ = activeAccountUnlocked; (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); - stateService.getEncryptedSends.calledWith(any()).mockResolvedValue({ - "1": sendData("1", "Test Send"), + accountService.activeAccountSubject.next({ + id: mockUserId, + email: "email", + name: "name", }); - stateService.getDecryptedSends - .calledWith(any()) - .mockResolvedValue([sendView("1", "Test Send")]); - - sendService = new SendService(cryptoService, i18nService, keyGenerationService, stateService); - }); - - afterEach(() => { - activeAccount.complete(); - activeAccountUnlocked.complete(); - }); - - describe("get", () => { - it("exists", async () => { - const result = sendService.get("1"); - - expect(result).toEqual(send("1", "Test Send")); + // Initial encrypted state + encryptedState = stateProvider.activeUser.getFake(SEND_USER_ENCRYPTED); + encryptedState.nextState({ + "1": testSendData("1", "Test Send"), }); + // Initial decrypted state + decryptedState = stateProvider.activeUser.getFake(SEND_USER_DECRYPTED); + decryptedState.nextState([testSendViewData("1", "Test Send")]); - it("does not exist", async () => { - const result = sendService.get("2"); - - expect(result).toBe(undefined); - }); + sendService = new SendService( + cryptoService, + i18nService, + keyGenerationService, + sendStateProvider, + encryptService, + ); }); describe("get$", () => { it("exists", async () => { const result = await firstValueFrom(sendService.get$("1")); - expect(result).toEqual(send("1", "Test Send")); + expect(result).toEqual(testSend("1", "Test Send")); }); it("does not exist", async () => { @@ -88,14 +99,14 @@ describe("SendService", () => { it("updated observable", async () => { const singleSendObservable = sendService.get$("1"); const result = await firstValueFrom(singleSendObservable); - expect(result).toEqual(send("1", "Test Send")); + expect(result).toEqual(testSend("1", "Test Send")); await sendService.replace({ - "1": sendData("1", "Test Send Updated"), + "1": testSendData("1", "Test Send Updated"), }); const result2 = await firstValueFrom(singleSendObservable); - expect(result2).toEqual(send("1", "Test Send Updated")); + expect(result2).toEqual(testSend("1", "Test Send Updated")); }); it("reports a change when name changes on a new send", async () => { @@ -103,13 +114,13 @@ describe("SendService", () => { sendService.get$("1").subscribe(() => { changed = true; }); - const sendDataObject = sendData("1", "Test Send 2"); + const sendDataObject = testSendData("1", "Test Send 2"); //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -120,7 +131,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); let changed = false; @@ -134,7 +145,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -145,7 +156,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); let changed = false; @@ -159,7 +170,7 @@ describe("SendService", () => { sendDataObject.text.text = "new text"; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -170,7 +181,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); let changed = false; @@ -184,7 +195,7 @@ describe("SendService", () => { sendDataObject.text = null; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -197,7 +208,7 @@ describe("SendService", () => { }) as SendData; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); sendDataObject.file = new SendFileData(new SendFileApi({ FileName: "updated name of file" })); @@ -211,7 +222,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(false); @@ -222,7 +233,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); let changed = false; @@ -236,7 +247,7 @@ describe("SendService", () => { sendDataObject.key = "newKey"; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -247,7 +258,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); let changed = false; @@ -261,7 +272,7 @@ describe("SendService", () => { sendDataObject.revisionDate = "2025-04-05"; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -272,7 +283,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); let changed = false; @@ -286,7 +297,7 @@ describe("SendService", () => { sendDataObject.name = null; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -299,7 +310,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); let changed = false; @@ -312,7 +323,7 @@ describe("SendService", () => { await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(false); @@ -320,7 +331,7 @@ describe("SendService", () => { sendDataObject.text.text = "Asdf"; await sendService.replace({ "1": sendDataObject, - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -332,14 +343,14 @@ describe("SendService", () => { changed = true; }); - const sendDataObject = sendData("1", "Test Send"); + const sendDataObject = testSendData("1", "Test Send"); //it is immediately called when subscribed, we need to reset the value changed = false; await sendService.replace({ "1": sendDataObject, - "2": sendData("3", "Test Send 3"), + "2": testSendData("3", "Test Send 3"), }); expect(changed).toEqual(false); @@ -354,7 +365,7 @@ describe("SendService", () => { changed = false; await sendService.replace({ - "2": sendData("2", "Test Send 2"), + "2": testSendData("2", "Test Send 2"), }); expect(changed).toEqual(true); @@ -366,14 +377,14 @@ describe("SendService", () => { const send1 = sends[0]; expect(sends).toHaveLength(1); - expect(send1).toEqual(send("1", "Test Send")); + expect(send1).toEqual(testSend("1", "Test Send")); }); describe("getFromState", () => { it("exists", async () => { const result = await sendService.getFromState("1"); - expect(result).toEqual(send("1", "Test Send")); + expect(result).toEqual(testSend("1", "Test Send")); }); it("does not exist", async () => { const result = await sendService.getFromState("2"); @@ -383,17 +394,17 @@ describe("SendService", () => { }); it("getAllDecryptedFromState", async () => { - await sendService.getAllDecryptedFromState(); + const sends = await sendService.getAllDecryptedFromState(); - expect(stateService.getDecryptedSends).toHaveBeenCalledTimes(1); + expect(sends[0]).toMatchObject(testSendViewData("1", "Test Send")); }); describe("getRotatedKeys", () => { let encryptedKey: EncString; beforeEach(() => { - cryptoService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); + encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Send Key"); - cryptoService.encrypt.mockResolvedValue(encryptedKey); + encryptService.encrypt.mockResolvedValue(encryptedKey); }); it("returns re-encrypted user sends", async () => { @@ -408,6 +419,8 @@ describe("SendService", () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises sendService.replace(null); + await awaitAsync(); + const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; const result = await sendService.getRotatedKeys(newUserKey); @@ -424,114 +437,51 @@ describe("SendService", () => { // InternalSendService it("upsert", async () => { - await sendService.upsert(sendData("2", "Test 2")); + await sendService.upsert(testSendData("2", "Test 2")); expect(await firstValueFrom(sendService.sends$)).toEqual([ - send("1", "Test Send"), - send("2", "Test 2"), + testSend("1", "Test Send"), + testSend("2", "Test 2"), ]); }); it("replace", async () => { - await sendService.replace({ "2": sendData("2", "test 2") }); + await sendService.replace({ "2": testSendData("2", "test 2") }); - expect(await firstValueFrom(sendService.sends$)).toEqual([send("2", "test 2")]); + expect(await firstValueFrom(sendService.sends$)).toEqual([testSend("2", "test 2")]); }); it("clear", async () => { await sendService.clear(); - + await awaitAsync(); expect(await firstValueFrom(sendService.sends$)).toEqual([]); }); + describe("Delete", () => { + it("Sends count should decrease after delete", async () => { + const sendsBeforeDelete = await firstValueFrom(sendService.sends$); + await sendService.delete(sendsBeforeDelete[0].id); - describe("delete", () => { - it("exists", async () => { - await sendService.delete("1"); - - expect(stateService.getEncryptedSends).toHaveBeenCalledTimes(2); - expect(stateService.setEncryptedSends).toHaveBeenCalledTimes(1); + const sendsAfterDelete = await firstValueFrom(sendService.sends$); + expect(sendsAfterDelete.length).toBeLessThan(sendsBeforeDelete.length); }); - it("does not exist", async () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - sendService.delete("1"); + it("Intended send should be delete", async () => { + const sendsBeforeDelete = await firstValueFrom(sendService.sends$); + await sendService.delete(sendsBeforeDelete[0].id); + const sendsAfterDelete = await firstValueFrom(sendService.sends$); + expect(sendsAfterDelete[0]).not.toBe(sendsBeforeDelete[0]); + }); - expect(stateService.getEncryptedSends).toHaveBeenCalledTimes(2); + it("Deleting on an empty sends array should not throw", async () => { + sendStateProvider.getEncryptedSends = jest.fn().mockResolvedValue(null); + await expect(sendService.delete("2")).resolves.not.toThrow(); + }); + + it("Delete multiple sends", async () => { + await sendService.upsert(testSendData("2", "send data 2")); + await sendService.delete(["1", "2"]); + const sendsAfterDelete = await firstValueFrom(sendService.sends$); + expect(sendsAfterDelete.length).toBe(0); }); }); - - // Send object helper functions - - function sendData(id: string, name: string) { - const data = new SendData({} as any); - data.id = id; - data.name = name; - data.disabled = false; - data.accessCount = 2; - data.accessId = "1"; - data.revisionDate = null; - data.expirationDate = null; - data.deletionDate = null; - data.notes = "Notes!!"; - data.key = null; - return data; - } - - const defaultSendData: Partial<SendData> = { - id: "1", - name: "Test Send", - accessId: "123", - type: SendType.Text, - notes: "notes!", - file: null, - text: new SendTextData(new SendTextApi({ Text: "send text" })), - key: "key", - maxAccessCount: 12, - accessCount: 2, - revisionDate: "2024-09-04", - expirationDate: "2024-09-04", - deletionDate: "2024-09-04", - password: "password", - disabled: false, - hideEmail: false, - }; - - function createSendData(value: Partial<SendData> = {}) { - const testSend: any = {}; - for (const prop in defaultSendData) { - testSend[prop] = value[prop as keyof SendData] ?? defaultSendData[prop as keyof SendData]; - } - return testSend; - } - - function sendView(id: string, name: string) { - const data = new SendView({} as any); - data.id = id; - data.name = name; - data.disabled = false; - data.accessCount = 2; - data.accessId = "1"; - data.revisionDate = null; - data.expirationDate = null; - data.deletionDate = null; - data.notes = "Notes!!"; - data.key = null; - return data; - } - - function send(id: string, name: string) { - const data = new Send({} as any); - data.id = id; - data.name = new EncString(name); - data.disabled = false; - data.accessCount = 2; - data.accessId = "1"; - data.revisionDate = null; - data.expirationDate = null; - data.deletionDate = null; - data.notes = new EncString("Notes!!"); - data.key = null; - return data; - } }); diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 528f90c1dc..33b1f28be0 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -1,9 +1,9 @@ -import { BehaviorSubject, Observable, concatMap, distinctUntilChanged, map } from "rxjs"; +import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; -import { StateService } from "../../../platform/abstractions/state.service"; import { KdfType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; @@ -19,48 +19,29 @@ import { SendWithIdRequest } from "../models/request/send-with-id.request"; import { SendView } from "../models/view/send.view"; import { SEND_KDF_ITERATIONS } from "../send-kdf"; +import { SendStateProvider } from "./send-state.provider.abstraction"; import { InternalSendService as InternalSendServiceAbstraction } from "./send.service.abstraction"; export class SendService implements InternalSendServiceAbstraction { readonly sendKeySalt = "bitwarden-send"; readonly sendKeyPurpose = "send"; - protected _sends: BehaviorSubject<Send[]> = new BehaviorSubject([]); - protected _sendViews: BehaviorSubject<SendView[]> = new BehaviorSubject([]); - - sends$ = this._sends.asObservable(); - sendViews$ = this._sendViews.asObservable(); + sends$ = this.stateProvider.encryptedState$.pipe( + map((record) => Object.values(record || {}).map((data) => new Send(data))), + ); + sendViews$ = this.stateProvider.encryptedState$.pipe( + concatMap((record) => + this.decryptSends(Object.values(record || {}).map((data) => new Send(data))), + ), + ); constructor( private cryptoService: CryptoService, private i18nService: I18nService, private keyGenerationService: KeyGenerationService, - private stateService: StateService, - ) { - this.stateService.activeAccountUnlocked$ - .pipe( - concatMap(async (unlocked) => { - if (Utils.global.bitwardenContainerService == null) { - return; - } - - if (!unlocked) { - this._sends.next([]); - this._sendViews.next([]); - return; - } - - const data = await this.stateService.getEncryptedSends(); - - await this.updateObservables(data); - }), - ) - .subscribe(); - } - - async clearCache(): Promise<void> { - await this._sendViews.next([]); - } + private stateProvider: SendStateProvider, + private encryptService: EncryptService, + ) {} async encrypt( model: SendView, @@ -93,12 +74,15 @@ export class SendService implements InternalSendServiceAbstraction { ); send.password = passwordKey.keyB64; } - send.key = await this.cryptoService.encrypt(model.key, key); - send.name = await this.cryptoService.encrypt(model.name, model.cryptoKey); - send.notes = await this.cryptoService.encrypt(model.notes, model.cryptoKey); + if (key == null) { + key = await this.cryptoService.getUserKey(); + } + send.key = await this.encryptService.encrypt(model.key, key); + send.name = await this.encryptService.encrypt(model.name, model.cryptoKey); + send.notes = await this.encryptService.encrypt(model.notes, model.cryptoKey); if (send.type === SendType.Text) { send.text = new SendText(); - send.text.text = await this.cryptoService.encrypt(model.text.text, model.cryptoKey); + send.text.text = await this.encryptService.encrypt(model.text.text, model.cryptoKey); send.text.hidden = model.text.hidden; } else if (send.type === SendType.File) { send.file = new SendFile(); @@ -120,11 +104,6 @@ export class SendService implements InternalSendServiceAbstraction { return [send, fileData]; } - get(id: string): Send { - const sends = this._sends.getValue(); - return sends.find((send) => send.id === id); - } - get$(id: string): Observable<Send | undefined> { return this.sends$.pipe( distinctUntilChanged((oldSends, newSends) => { @@ -188,7 +167,7 @@ export class SendService implements InternalSendServiceAbstraction { } async getFromState(id: string): Promise<Send> { - const sends = await this.stateService.getEncryptedSends(); + const sends = await this.stateProvider.getEncryptedSends(); // eslint-disable-next-line if (sends == null || !sends.hasOwnProperty(id)) { return null; @@ -198,7 +177,7 @@ export class SendService implements InternalSendServiceAbstraction { } async getAll(): Promise<Send[]> { - const sends = await this.stateService.getEncryptedSends(); + const sends = await this.stateProvider.getEncryptedSends(); const response: Send[] = []; for (const id in sends) { // eslint-disable-next-line @@ -210,7 +189,7 @@ export class SendService implements InternalSendServiceAbstraction { } async getAllDecryptedFromState(): Promise<SendView[]> { - let decSends = await this.stateService.getDecryptedSends(); + let decSends = await this.stateProvider.getDecryptedSends(); if (decSends != null) { return decSends; } @@ -230,12 +209,12 @@ export class SendService implements InternalSendServiceAbstraction { await Promise.all(promises); decSends.sort(Utils.getSortFunction(this.i18nService, "name")); - await this.stateService.setDecryptedSends(decSends); + await this.stateProvider.setDecryptedSends(decSends); return decSends; } async upsert(send: SendData | SendData[]): Promise<any> { - let sends = await this.stateService.getEncryptedSends(); + let sends = await this.stateProvider.getEncryptedSends(); if (sends == null) { sends = {}; } @@ -252,16 +231,12 @@ export class SendService implements InternalSendServiceAbstraction { } async clear(userId?: string): Promise<any> { - if (userId == null || userId == (await this.stateService.getUserId())) { - this._sends.next([]); - this._sendViews.next([]); - } - await this.stateService.setDecryptedSends(null, { userId: userId }); - await this.stateService.setEncryptedSends(null, { userId: userId }); + await this.stateProvider.setDecryptedSends(null); + await this.stateProvider.setEncryptedSends(null); } async delete(id: string | string[]): Promise<any> { - const sends = await this.stateService.getEncryptedSends(); + const sends = await this.stateProvider.getEncryptedSends(); if (sends == null) { return; } @@ -281,8 +256,7 @@ export class SendService implements InternalSendServiceAbstraction { } async replace(sends: { [id: string]: SendData }): Promise<any> { - await this.updateObservables(sends); - await this.stateService.setEncryptedSends(sends); + await this.stateProvider.setEncryptedSends(sends); } async getRotatedKeys(newUserKey: UserKey): Promise<SendWithIdRequest[]> { @@ -290,14 +264,21 @@ export class SendService implements InternalSendServiceAbstraction { throw new Error("New user key is required for rotation."); } + const req = await firstValueFrom( + this.sends$.pipe(concatMap(async (sends) => this.toRotatedKeyRequestMap(sends, newUserKey))), + ); + // separate return for easier debugging + return req; + } + + private async toRotatedKeyRequestMap(sends: Send[], newUserKey: UserKey) { const requests = await Promise.all( - this._sends.value.map(async (send) => { - const sendKey = await this.cryptoService.decryptToBytes(send.key); - send.key = await this.cryptoService.encrypt(sendKey, newUserKey); + sends.map(async (send) => { + const sendKey = await this.encryptService.decryptToBytes(send.key, newUserKey); + send.key = await this.encryptService.encrypt(sendKey, newUserKey); return new SendWithIdRequest(send); }), ); - // separate return for easier debugging return requests; } @@ -329,18 +310,12 @@ export class SendService implements InternalSendServiceAbstraction { data: ArrayBuffer, key: SymmetricCryptoKey, ): Promise<[EncString, EncArrayBuffer]> { - const encFileName = await this.cryptoService.encrypt(fileName, key); - const encFileData = await this.cryptoService.encryptToBytes(new Uint8Array(data), key); - return [encFileName, encFileData]; - } - - private async updateObservables(sendsMap: { [id: string]: SendData }) { - const sends = Object.values(sendsMap || {}).map((f) => new Send(f)); - this._sends.next(sends); - - if (await this.cryptoService.hasUserKey()) { - this._sendViews.next(await this.decryptSends(sends)); + if (key == null) { + key = await this.cryptoService.getUserKey(); } + const encFileName = await this.encryptService.encrypt(fileName, key); + const encFileData = await this.encryptService.encryptToBytes(new Uint8Array(data), key); + return [encFileName, encFileData]; } private async decryptSends(sends: Send[]) { diff --git a/libs/common/src/tools/send/services/test-data/send-tests.data.ts b/libs/common/src/tools/send/services/test-data/send-tests.data.ts new file mode 100644 index 0000000000..a57a39782e --- /dev/null +++ b/libs/common/src/tools/send/services/test-data/send-tests.data.ts @@ -0,0 +1,79 @@ +import { EncString } from "../../../../platform/models/domain/enc-string"; +import { SendType } from "../../enums/send-type"; +import { SendTextApi } from "../../models/api/send-text.api"; +import { SendTextData } from "../../models/data/send-text.data"; +import { SendData } from "../../models/data/send.data"; +import { Send } from "../../models/domain/send"; +import { SendView } from "../../models/view/send.view"; + +export function testSendViewData(id: string, name: string) { + const data = new SendView({} as any); + data.id = id; + data.name = name; + data.disabled = false; + data.accessCount = 2; + data.accessId = "1"; + data.revisionDate = null; + data.expirationDate = null; + data.deletionDate = null; + data.notes = "Notes!!"; + data.key = null; + return data; +} + +export function createSendData(value: Partial<SendData> = {}) { + const defaultSendData: Partial<SendData> = { + id: "1", + name: "Test Send", + accessId: "123", + type: SendType.Text, + notes: "notes!", + file: null, + text: new SendTextData(new SendTextApi({ Text: "send text" })), + key: "key", + maxAccessCount: 12, + accessCount: 2, + revisionDate: "2024-09-04", + expirationDate: "2024-09-04", + deletionDate: "2024-09-04", + password: "password", + disabled: false, + hideEmail: false, + }; + + const testSend: any = {}; + for (const prop in defaultSendData) { + testSend[prop] = value[prop as keyof SendData] ?? defaultSendData[prop as keyof SendData]; + } + return testSend; +} + +export function testSendData(id: string, name: string) { + const data = new SendData({} as any); + data.id = id; + data.name = name; + data.disabled = false; + data.accessCount = 2; + data.accessId = "1"; + data.revisionDate = null; + data.expirationDate = null; + data.deletionDate = null; + data.notes = "Notes!!"; + data.key = null; + return data; +} + +export function testSend(id: string, name: string) { + const data = new Send({} as any); + data.id = id; + data.name = new EncString(name); + data.disabled = false; + data.accessCount = 2; + data.accessId = "1"; + data.revisionDate = null; + data.expirationDate = null; + data.deletionDate = null; + data.notes = new EncString("Notes!!"); + data.key = null; + return data; +} diff --git a/libs/common/src/types/guid.ts b/libs/common/src/types/guid.ts index 7f88c82a9e..97c87e684e 100644 --- a/libs/common/src/types/guid.ts +++ b/libs/common/src/types/guid.ts @@ -7,3 +7,5 @@ export type OrganizationId = Opaque<string, "OrganizationId">; export type CollectionId = Opaque<string, "CollectionId">; export type ProviderId = Opaque<string, "ProviderId">; export type PolicyId = Opaque<string, "PolicyId">; +export type CipherId = Opaque<string, "CipherId">; +export type IndexedEntityId = Opaque<string, "IndexedEntityId">; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 30b518612d..501fd87665 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,13 +1,21 @@ +import { Observable } from "rxjs"; + import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; import { Cipher } from "../models/domain/cipher"; import { Field } from "../models/domain/field"; import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; +import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; export abstract class CipherService { + /** + * An observable monitoring the add/edit cipher info saved to memory. + */ + addEditCipherInfo$: Observable<AddEditCipherInfo>; clearCache: (userId?: string) => Promise<void>; encrypt: ( model: CipherView, @@ -63,6 +71,19 @@ export abstract class CipherService { admin?: boolean, ) => Promise<Cipher>; saveCollectionsWithServer: (cipher: Cipher) => Promise<any>; + /** + * Bulk update collections for many ciphers with the server + * @param orgId + * @param cipherIds + * @param collectionIds + * @param removeCollections - If true, the collections will be removed from the ciphers, otherwise they will be added + */ + bulkUpdateCollectionsWithServer: ( + orgId: OrganizationId, + cipherIds: CipherId[], + collectionIds: CollectionId[], + removeCollections: boolean, + ) => Promise<void>; upsert: (cipher: CipherData | CipherData[]) => Promise<any>; replace: (ciphers: { [id: string]: CipherData }) => Promise<any>; clear: (userId: string) => Promise<any>; @@ -88,4 +109,5 @@ export abstract class CipherService { asAdmin?: boolean, ) => Promise<void>; getKeyForCipherKeyDecryption: (cipher: Cipher) => Promise<any>; + setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>; } diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index 1452ffe7ee..f8db7186d6 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; import { CipherResponse } from "../response/cipher.response"; @@ -84,4 +86,8 @@ export class CipherData { this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph)); } } + + static fromJSON(obj: Jsonify<CipherData>) { + return Object.assign(new CipherData(), obj); + } } diff --git a/libs/common/src/vault/models/request/cipher-bulk-update-collections.request.ts b/libs/common/src/vault/models/request/cipher-bulk-update-collections.request.ts new file mode 100644 index 0000000000..1b1a77a48d --- /dev/null +++ b/libs/common/src/vault/models/request/cipher-bulk-update-collections.request.ts @@ -0,0 +1,19 @@ +import { CipherId, CollectionId, OrganizationId } from "../../../types/guid"; + +export class CipherBulkUpdateCollectionsRequest { + organizationId: OrganizationId; + cipherIds: CipherId[]; + collectionIds: CollectionId[]; + removeCollections: boolean; + constructor( + organizationId: OrganizationId, + cipherIds: CipherId[], + collectionIds: CollectionId[], + removeCollections: boolean = false, + ) { + this.organizationId = organizationId; + this.cipherIds = cipherIds; + this.collectionIds = collectionIds; + this.removeCollections = removeCollections; + } +} diff --git a/libs/common/src/vault/models/response/collection.response.ts b/libs/common/src/vault/models/response/collection.response.ts index f16fe547e0..ac4781df71 100644 --- a/libs/common/src/vault/models/response/collection.response.ts +++ b/libs/common/src/vault/models/response/collection.response.ts @@ -21,6 +21,10 @@ export class CollectionDetailsResponse extends CollectionResponse { readOnly: boolean; manage: boolean; hidePasswords: boolean; + + /** + * Flag indicating the user has been explicitly assigned to this Collection + */ assigned: boolean; constructor(response: any) { @@ -35,15 +39,10 @@ export class CollectionDetailsResponse extends CollectionResponse { } } -export class CollectionAccessDetailsResponse extends CollectionResponse { +export class CollectionAccessDetailsResponse extends CollectionDetailsResponse { groups: SelectionReadOnlyResponse[] = []; users: SelectionReadOnlyResponse[] = []; - /** - * Flag indicating the user has been explicitly assigned to this Collection - */ - assigned: boolean; - constructor(response: any) { super(response); this.assigned = this.getResponseProperty("Assigned") || false; diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 74d369380b..86766bdeac 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -53,11 +53,11 @@ export class CollectionView implements View, ITreeNodeObject { ); } - return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned); + return org?.canEditAnyCollection(false) || (org?.canEditAssignedCollections && this.assigned); } // For editing collection details, not the items within it. - canEdit(org: Organization): boolean { + canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { if (org != null && org.id !== this.organizationId) { throw new Error( "Id of the organization provided does not match the org id of the collection.", @@ -65,8 +65,8 @@ export class CollectionView implements View, ITreeNodeObject { } return org?.flexibleCollections - ? org?.canEditAnyCollection || this.manage - : org?.canEditAnyCollection || org?.canEditAssignedCollections; + ? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage + : org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || org?.canEditAssignedCollections; } // For deleting a collection, not the items within it. diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index bcd4bb9836..28c4bfc653 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,21 +1,25 @@ import { mock } from "jest-mock-extended"; import { of } from "rxjs"; +import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; +import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; -import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { Utils } from "../../platform/misc/utils"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../platform/services/container.service"; +import { UserId } from "../../types/guid"; import { CipherKey, OrgKey } from "../../types/key"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; @@ -97,6 +101,8 @@ const cipherData: CipherData = { }, ], }; +const mockUserId = Utils.newGuid() as UserId; +let accountService: FakeAccountService; describe("Cipher Service", () => { const cryptoService = mock<CryptoService>(); @@ -108,7 +114,9 @@ describe("Cipher Service", () => { const i18nService = mock<I18nService>(); const searchService = mock<SearchService>(); const encryptService = mock<EncryptService>(); - const configService = mock<ConfigServiceAbstraction>(); + const configService = mock<ConfigService>(); + accountService = mockAccountServiceWith(mockUserId); + const stateProvider = new FakeStateProvider(accountService); let cipherService: CipherService; let cipherObj: Cipher; @@ -130,6 +138,7 @@ describe("Cipher Service", () => { encryptService, cipherFileUploadService, configService, + stateProvider, ); cipherObj = new Cipher(cipherData); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8a86d9aa05..7d06b3185f 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { Observable, firstValueFrom, map } from "rxjs"; import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; @@ -9,7 +9,7 @@ import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; -import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -21,12 +21,15 @@ import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { ActiveUserState, StateProvider } from "../../platform/state"; +import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; import { UserKey, OrgKey } from "../../types/key"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { FieldType } from "../enums"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; +import { LocalData } from "../models/data/local.data"; import { Attachment } from "../models/domain/attachment"; import { Card } from "../models/domain/card"; import { Cipher } from "../models/domain/cipher"; @@ -42,6 +45,7 @@ import { CipherBulkDeleteRequest } from "../models/request/cipher-bulk-delete.re import { CipherBulkMoveRequest } from "../models/request/cipher-bulk-move.request"; import { CipherBulkRestoreRequest } from "../models/request/cipher-bulk-restore.request"; import { CipherBulkShareRequest } from "../models/request/cipher-bulk-share.request"; +import { CipherBulkUpdateCollectionsRequest } from "../models/request/cipher-bulk-update-collections.request"; import { CipherCollectionsRequest } from "../models/request/cipher-collections.request"; import { CipherCreateRequest } from "../models/request/cipher-create.request"; import { CipherPartialRequest } from "../models/request/cipher-partial.request"; @@ -52,6 +56,14 @@ import { AttachmentView } from "../models/view/attachment.view"; import { CipherView } from "../models/view/cipher.view"; import { FieldView } from "../models/view/field.view"; import { PasswordHistoryView } from "../models/view/password-history.view"; +import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; + +import { + ENCRYPTED_CIPHERS, + LOCAL_DATA_KEY, + ADD_EDIT_CIPHER_INFO_KEY, + DECRYPTED_CIPHERS, +} from "./key-state/ciphers.state"; const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0"); @@ -60,6 +72,16 @@ export class CipherService implements CipherServiceAbstraction { this.sortCiphersByLastUsed, ); + localData$: Observable<Record<CipherId, LocalData>>; + ciphers$: Observable<Record<CipherId, CipherData>>; + cipherViews$: Observable<Record<CipherId, CipherView>>; + addEditCipherInfo$: Observable<AddEditCipherInfo>; + + private localDataState: ActiveUserState<Record<CipherId, LocalData>>; + private encryptedCiphersState: ActiveUserState<Record<CipherId, CipherData>>; + private decryptedCiphersState: ActiveUserState<Record<CipherId, CipherView>>; + private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>; + constructor( private cryptoService: CryptoService, private domainSettingsService: DomainSettingsService, @@ -70,12 +92,18 @@ export class CipherService implements CipherServiceAbstraction { private autofillSettingsService: AutofillSettingsServiceAbstraction, private encryptService: EncryptService, private cipherFileUploadService: CipherFileUploadService, - private configService: ConfigServiceAbstraction, - ) {} + private configService: ConfigService, + private stateProvider: StateProvider, + ) { + this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); + this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); + this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); + this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); - async getDecryptedCipherCache(): Promise<CipherView[]> { - const decryptedCiphers = await this.stateService.getDecryptedCiphers(); - return decryptedCiphers; + this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); + this.ciphers$ = this.encryptedCiphersState.state$.pipe(map((ciphers) => ciphers ?? {})); + this.cipherViews$ = this.decryptedCiphersState.state$.pipe(map((views) => views ?? {})); + this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } async setDecryptedCipherCache(value: CipherView[]) { @@ -83,17 +111,25 @@ export class CipherService implements CipherServiceAbstraction { // if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again. // We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption. if (value == null || value.length !== 0) { - await this.stateService.setDecryptedCiphers(value); + await this.setDecryptedCiphers(value); } if (this.searchService != null) { if (value == null) { - this.searchService.clearIndex(); + await this.searchService.clearIndex(); } else { - this.searchService.indexCiphers(value); + await this.searchService.indexCiphers(value); } } } + private async setDecryptedCiphers(value: CipherView[]) { + const cipherViews: { [id: string]: CipherView } = {}; + value?.forEach((c) => { + cipherViews[c.id] = c; + }); + await this.decryptedCiphersState.update(() => cipherViews); + } + async clearCache(userId?: string): Promise<void> { await this.clearDecryptedCiphersState(userId); } @@ -266,24 +302,27 @@ export class CipherService implements CipherServiceAbstraction { } async get(id: string): Promise<Cipher> { - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await firstValueFrom(this.ciphers$); // eslint-disable-next-line if (ciphers == null || !ciphers.hasOwnProperty(id)) { return null; } - const localData = await this.stateService.getLocalData(); - return new Cipher(ciphers[id], localData ? localData[id] : null); + const localData = await firstValueFrom(this.localData$); + const cipherId = id as CipherId; + + return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null); } async getAll(): Promise<Cipher[]> { - const localData = await this.stateService.getLocalData(); - const ciphers = await this.stateService.getEncryptedCiphers(); + const localData = await firstValueFrom(this.localData$); + const ciphers = await firstValueFrom(this.ciphers$); const response: Cipher[] = []; for (const id in ciphers) { // eslint-disable-next-line if (ciphers.hasOwnProperty(id)) { - response.push(new Cipher(ciphers[id], localData ? localData[id] : null)); + const cipherId = id as CipherId; + response.push(new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null)); } } return response; @@ -291,12 +330,23 @@ export class CipherService implements CipherServiceAbstraction { @sequentialize(() => "getAllDecrypted") async getAllDecrypted(): Promise<CipherView[]> { - if ((await this.getDecryptedCipherCache()) != null) { + let decCiphers = await this.getDecryptedCiphers(); + if (decCiphers != null && decCiphers.length !== 0) { await this.reindexCiphers(); - return await this.getDecryptedCipherCache(); + return await this.getDecryptedCiphers(); } - const ciphers = await this.getAll(); + decCiphers = await this.decryptCiphers(await this.getAll()); + + await this.setDecryptedCipherCache(decCiphers); + return decCiphers; + } + + private async getDecryptedCiphers() { + return Object.values(await firstValueFrom(this.cipherViews$)); + } + + private async decryptCiphers(ciphers: Cipher[]) { const orgKeys = await this.cryptoService.getOrgKeys(); const userKey = await this.cryptoService.getUserKeyWithLegacySupport(); if (Object.keys(orgKeys).length === 0 && userKey == null) { @@ -324,16 +374,16 @@ export class CipherService implements CipherServiceAbstraction { .flat() .sort(this.getLocaleSortingFunction()); - await this.setDecryptedCipherCache(decCiphers); return decCiphers; } private async reindexCiphers() { const userId = await this.stateService.getUserId(); const reindexRequired = - this.searchService != null && (this.searchService.indexedEntityId ?? userId) !== userId; + this.searchService != null && + ((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId; if (reindexRequired) { - this.searchService.indexCiphers(await this.getDecryptedCipherCache(), userId); + await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId); } } @@ -445,22 +495,24 @@ export class CipherService implements CipherServiceAbstraction { } async updateLastUsedDate(id: string): Promise<void> { - let ciphersLocalData = await this.stateService.getLocalData(); + let ciphersLocalData = await firstValueFrom(this.localData$); + if (!ciphersLocalData) { ciphersLocalData = {}; } - if (ciphersLocalData[id]) { - ciphersLocalData[id].lastUsedDate = new Date().getTime(); + const cipherId = id as CipherId; + if (ciphersLocalData[cipherId]) { + ciphersLocalData[cipherId].lastUsedDate = new Date().getTime(); } else { - ciphersLocalData[id] = { + ciphersLocalData[cipherId] = { lastUsedDate: new Date().getTime(), }; } - await this.stateService.setLocalData(ciphersLocalData); + await this.localDataState.update(() => ciphersLocalData); - const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + const decryptedCipherCache = await this.getDecryptedCiphers(); if (!decryptedCipherCache) { return; } @@ -468,30 +520,32 @@ export class CipherService implements CipherServiceAbstraction { for (let i = 0; i < decryptedCipherCache.length; i++) { const cached = decryptedCipherCache[i]; if (cached.id === id) { - cached.localData = ciphersLocalData[id]; + cached.localData = ciphersLocalData[id as CipherId]; break; } } - await this.stateService.setDecryptedCiphers(decryptedCipherCache); + await this.setDecryptedCiphers(decryptedCipherCache); } async updateLastLaunchedDate(id: string): Promise<void> { - let ciphersLocalData = await this.stateService.getLocalData(); + let ciphersLocalData = await firstValueFrom(this.localData$); + if (!ciphersLocalData) { ciphersLocalData = {}; } - if (ciphersLocalData[id]) { - ciphersLocalData[id].lastLaunched = new Date().getTime(); + const cipherId = id as CipherId; + if (ciphersLocalData[cipherId]) { + ciphersLocalData[cipherId].lastLaunched = new Date().getTime(); } else { - ciphersLocalData[id] = { + ciphersLocalData[cipherId] = { lastUsedDate: new Date().getTime(), }; } - await this.stateService.setLocalData(ciphersLocalData); + await this.localDataState.update(() => ciphersLocalData); - const decryptedCipherCache = await this.stateService.getDecryptedCiphers(); + const decryptedCipherCache = await this.getDecryptedCiphers(); if (!decryptedCipherCache) { return; } @@ -499,11 +553,11 @@ export class CipherService implements CipherServiceAbstraction { for (let i = 0; i < decryptedCipherCache.length; i++) { const cached = decryptedCipherCache[i]; if (cached.id === id) { - cached.localData = ciphersLocalData[id]; + cached.localData = ciphersLocalData[id as CipherId]; break; } } - await this.stateService.setDecryptedCiphers(decryptedCipherCache); + await this.setDecryptedCiphers(decryptedCipherCache); } async saveNeverDomain(domain: string): Promise<void> { @@ -680,32 +734,74 @@ export class CipherService implements CipherServiceAbstraction { async saveCollectionsWithServer(cipher: Cipher): Promise<any> { const request = new CipherCollectionsRequest(cipher.collectionIds); - await this.apiService.putCipherCollections(cipher.id, request); - const data = cipher.toCipherData(); + const response = await this.apiService.putCipherCollections(cipher.id, request); + const data = new CipherData(response); await this.upsert(data); } + /** + * Bulk update collections for many ciphers with the server + * @param orgId + * @param cipherIds + * @param collectionIds + * @param removeCollections - If true, the collectionIds will be removed from the ciphers, otherwise they will be added + */ + async bulkUpdateCollectionsWithServer( + orgId: OrganizationId, + cipherIds: CipherId[], + collectionIds: CollectionId[], + removeCollections: boolean = false, + ): Promise<void> { + const request = new CipherBulkUpdateCollectionsRequest( + orgId, + cipherIds, + collectionIds, + removeCollections, + ); + + await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false); + + // Update the local state + const ciphers = await firstValueFrom(this.ciphers$); + + for (const id of cipherIds) { + const cipher = ciphers[id]; + if (cipher) { + if (removeCollections) { + cipher.collectionIds = cipher.collectionIds?.filter( + (cid) => !collectionIds.includes(cid as CollectionId), + ); + } else { + // Append to the collectionIds if it's not already there + cipher.collectionIds = [...new Set([...(cipher.collectionIds ?? []), ...collectionIds])]; + } + } + } + + await this.clearCache(); + await this.encryptedCiphersState.update(() => ciphers); + } + async upsert(cipher: CipherData | CipherData[]): Promise<any> { - let ciphers = await this.stateService.getEncryptedCiphers(); - if (ciphers == null) { - ciphers = {}; - } - - if (cipher instanceof CipherData) { - const c = cipher as CipherData; - ciphers[c.id] = c; - } else { - (cipher as CipherData[]).forEach((c) => { - ciphers[c.id] = c; - }); - } - - await this.replace(ciphers); + const ciphers = cipher instanceof CipherData ? [cipher] : cipher; + await this.updateEncryptedCipherState((current) => { + ciphers.forEach((c) => (current[c.id as CipherId] = c)); + return current; + }); } async replace(ciphers: { [id: string]: CipherData }): Promise<any> { + await this.updateEncryptedCipherState(() => ciphers); + } + + private async updateEncryptedCipherState( + update: (current: Record<CipherId, CipherData>) => Record<CipherId, CipherData>, + ) { await this.clearDecryptedCiphersState(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update((current) => { + const result = update(current ?? {}); + return result; + }); } async clear(userId?: string): Promise<any> { @@ -716,7 +812,7 @@ export class CipherService implements CipherServiceAbstraction { async moveManyWithServer(ids: string[], folderId: string): Promise<any> { await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); - let ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { ciphers = {}; } @@ -724,33 +820,34 @@ export class CipherService implements CipherServiceAbstraction { ids.forEach((id) => { // eslint-disable-next-line if (ciphers.hasOwnProperty(id)) { - ciphers[id].folderId = folderId; + ciphers[id as CipherId].folderId = folderId; } }); await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => ciphers); } async delete(id: string | string[]): Promise<any> { - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } if (typeof id === "string") { - if (ciphers[id] == null) { + const cipherId = id as CipherId; + if (ciphers[cipherId] == null) { return; } - delete ciphers[id]; + delete ciphers[cipherId]; } else { - (id as string[]).forEach((i) => { + (id as CipherId[]).forEach((i) => { delete ciphers[i]; }); } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => ciphers); } async deleteWithServer(id: string, asAdmin = false): Promise<any> { @@ -774,21 +871,26 @@ export class CipherService implements CipherServiceAbstraction { } async deleteAttachment(id: string, attachmentId: string): Promise<void> { - const ciphers = await this.stateService.getEncryptedCiphers(); - + let ciphers = await firstValueFrom(this.ciphers$); + const cipherId = id as CipherId; // eslint-disable-next-line - if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[id].attachments == null) { + if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) { return; } - for (let i = 0; i < ciphers[id].attachments.length; i++) { - if (ciphers[id].attachments[i].id === attachmentId) { - ciphers[id].attachments.splice(i, 1); + for (let i = 0; i < ciphers[cipherId].attachments.length; i++) { + if (ciphers[cipherId].attachments[i].id === attachmentId) { + ciphers[cipherId].attachments.splice(i, 1); } } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<void> { @@ -871,12 +973,12 @@ export class CipherService implements CipherServiceAbstraction { } async softDelete(id: string | string[]): Promise<any> { - const ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } - const setDeletedDate = (cipherId: string) => { + const setDeletedDate = (cipherId: CipherId) => { if (ciphers[cipherId] == null) { return; } @@ -884,13 +986,18 @@ export class CipherService implements CipherServiceAbstraction { }; if (typeof id === "string") { - setDeletedDate(id); + setDeletedDate(id as CipherId); } else { (id as string[]).forEach(setDeletedDate); } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async softDeleteWithServer(id: string, asAdmin = false): Promise<any> { @@ -917,17 +1024,18 @@ export class CipherService implements CipherServiceAbstraction { async restore( cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], ) { - const ciphers = await this.stateService.getEncryptedCiphers(); + let ciphers = await firstValueFrom(this.ciphers$); if (ciphers == null) { return; } const clearDeletedDate = (c: { id: string; revisionDate: string }) => { - if (ciphers[c.id] == null) { + const cipherId = c.id as CipherId; + if (ciphers[cipherId] == null) { return; } - ciphers[c.id].deletedDate = null; - ciphers[c.id].revisionDate = c.revisionDate; + ciphers[cipherId].deletedDate = null; + ciphers[cipherId].revisionDate = c.revisionDate; }; if (cipher.constructor.name === Array.name) { @@ -937,7 +1045,12 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.stateService.setEncryptedCiphers(ciphers); + await this.encryptedCiphersState.update(() => { + if (ciphers == null) { + ciphers = {}; + } + return ciphers; + }); } async restoreWithServer(id: string, asAdmin = false): Promise<any> { @@ -979,6 +1092,10 @@ export class CipherService implements CipherServiceAbstraction { ); } + async setAddEditCipherInfo(value: AddEditCipherInfo) { + await this.addEditCipherInfoState.update(() => value); + } + // Helpers // In the case of a cipher that is being shared with an organization, we want to decrypt the @@ -1304,11 +1421,11 @@ export class CipherService implements CipherServiceAbstraction { } private async clearEncryptedCiphersState(userId?: string) { - await this.stateService.setEncryptedCiphers(null, { userId: userId }); + await this.encryptedCiphersState.update(() => ({})); } private async clearDecryptedCiphersState(userId?: string) { - await this.stateService.setDecryptedCiphers(null, { userId: userId }); + await this.setDecryptedCiphers(null); this.clearSortedCiphers(); } @@ -1342,7 +1459,6 @@ export class CipherService implements CipherServiceAbstraction { cipher.attachments = attachments; }), ]); - return cipher; } diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts index 2f76c5043a..9757e24d8f 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts @@ -4,7 +4,7 @@ import { of } from "rxjs"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; -import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../../platform/abstractions/config/config.service"; import { Utils } from "../../../platform/misc/utils"; import { Fido2AuthenticatorError, @@ -30,7 +30,7 @@ const VaultUrl = "https://vault.bitwarden.com"; describe("FidoAuthenticatorService", () => { let authenticator!: MockProxy<Fido2AuthenticatorService>; - let configService!: MockProxy<ConfigServiceAbstraction>; + let configService!: MockProxy<ConfigService>; let authService!: MockProxy<AuthService>; let vaultSettingsService: MockProxy<VaultSettingsService>; let domainSettingsService: MockProxy<DomainSettingsService>; @@ -39,7 +39,7 @@ describe("FidoAuthenticatorService", () => { beforeEach(async () => { authenticator = mock<Fido2AuthenticatorService>(); - configService = mock<ConfigServiceAbstraction>(); + configService = mock<ConfigService>(); authService = mock<AuthService>(); vaultSettingsService = mock<VaultSettingsService>(); domainSettingsService = mock<DomainSettingsService>(); diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.ts b/libs/common/src/vault/services/fido2/fido2-client.service.ts index c725b22637..bfc8cbe915 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -4,7 +4,7 @@ import { parse } from "tldts"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; -import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; +import { ConfigService } from "../../../platform/abstractions/config/config.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { Utils } from "../../../platform/misc/utils"; import { @@ -40,7 +40,7 @@ import { Fido2Utils } from "./fido2-utils"; export class Fido2ClientService implements Fido2ClientServiceAbstraction { constructor( private authenticator: Fido2AuthenticatorService, - private configService: ConfigServiceAbstraction, + private configService: ConfigService, private authService: AuthService, private vaultSettingsService: VaultSettingsService, private domainSettingsService: DomainSettingsService, diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 88595720e2..8c3be9abe8 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -8,7 +8,6 @@ import { FakeStateProvider } from "../../../../spec/fake-state-provider"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { StateService } from "../../../platform/abstractions/state.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -27,7 +26,6 @@ describe("Folder Service", () => { let encryptService: MockProxy<EncryptService>; let i18nService: MockProxy<I18nService>; let cipherService: MockProxy<CipherService>; - let stateService: MockProxy<StateService>; let stateProvider: FakeStateProvider; const mockUserId = Utils.newGuid() as UserId; @@ -39,7 +37,6 @@ describe("Folder Service", () => { encryptService = mock<EncryptService>(); i18nService = mock<I18nService>(); cipherService = mock<CipherService>(); - stateService = mock<StateService>(); accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); @@ -52,13 +49,7 @@ describe("Folder Service", () => { ); encryptService.decryptToUtf8.mockResolvedValue("DEC"); - folderService = new FolderService( - cryptoService, - i18nService, - cipherService, - stateService, - stateProvider, - ); + folderService = new FolderService(cryptoService, i18nService, cipherService, stateProvider); folderState = stateProvider.activeUser.getFake(FOLDER_ENCRYPTED_FOLDERS); diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index afe3b01c68..584567aee8 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,17 +2,16 @@ import { Observable, firstValueFrom, map } from "rxjs"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { StateService } from "../../../platform/abstractions/state.service"; import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ActiveUserState, DerivedState, StateProvider } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { InternalFolderService as InternalFolderServiceAbstraction } from "../../../vault/abstractions/folder/folder.service.abstraction"; -import { CipherData } from "../../../vault/models/data/cipher.data"; import { FolderData } from "../../../vault/models/data/folder.data"; import { Folder } from "../../../vault/models/domain/folder"; import { FolderView } from "../../../vault/models/view/folder.view"; +import { Cipher } from "../../models/domain/cipher"; import { FOLDER_DECRYPTED_FOLDERS, FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state"; export class FolderService implements InternalFolderServiceAbstraction { @@ -26,7 +25,6 @@ export class FolderService implements InternalFolderServiceAbstraction { private cryptoService: CryptoService, private i18nService: I18nService, private cipherService: CipherService, - private stateService: StateService, private stateProvider: StateProvider, ) { this.encryptedFoldersState = this.stateProvider.getActive(FOLDER_ENCRYPTED_FOLDERS); @@ -144,9 +142,9 @@ export class FolderService implements InternalFolderServiceAbstraction { }); // Items in a deleted folder are re-assigned to "No Folder" - const ciphers = await this.stateService.getEncryptedCiphers(); + const ciphers = await this.cipherService.getAll(); if (ciphers != null) { - const updates: CipherData[] = []; + const updates: Cipher[] = []; for (const cId in ciphers) { if (ciphers[cId].folderId === id) { ciphers[cId].folderId = null; @@ -156,7 +154,7 @@ export class FolderService implements InternalFolderServiceAbstraction { if (updates.length > 0) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.cipherService.upsert(updates); + this.cipherService.upsert(updates.map((c) => c.toCipherData())); } } } diff --git a/libs/common/src/vault/services/key-state/ciphers.state.ts b/libs/common/src/vault/services/key-state/ciphers.state.ts new file mode 100644 index 0000000000..71da4c2333 --- /dev/null +++ b/libs/common/src/vault/services/key-state/ciphers.state.ts @@ -0,0 +1,52 @@ +import { Jsonify } from "type-fest"; + +import { + CIPHERS_DISK, + CIPHERS_DISK_LOCAL, + CIPHERS_MEMORY, + KeyDefinition, +} from "../../../platform/state"; +import { CipherId } from "../../../types/guid"; +import { CipherData } from "../../models/data/cipher.data"; +import { LocalData } from "../../models/data/local.data"; +import { CipherView } from "../../models/view/cipher.view"; +import { AddEditCipherInfo } from "../../types/add-edit-cipher-info"; + +export const ENCRYPTED_CIPHERS = KeyDefinition.record<CipherData>(CIPHERS_DISK, "ciphers", { + deserializer: (obj: Jsonify<CipherData>) => CipherData.fromJSON(obj), +}); + +export const DECRYPTED_CIPHERS = KeyDefinition.record<CipherView>( + CIPHERS_MEMORY, + "decryptedCiphers", + { + deserializer: (cipher: Jsonify<CipherView>) => CipherView.fromJSON(cipher), + }, +); + +export const LOCAL_DATA_KEY = new KeyDefinition<Record<CipherId, LocalData>>( + CIPHERS_DISK_LOCAL, + "localData", + { + deserializer: (localData) => localData, + }, +); + +export const ADD_EDIT_CIPHER_INFO_KEY = new KeyDefinition<AddEditCipherInfo>( + CIPHERS_MEMORY, + "addEditCipherInfo", + { + deserializer: (addEditCipherInfo: AddEditCipherInfo) => { + if (addEditCipherInfo == null) { + return null; + } + + const cipher = + addEditCipherInfo?.cipher.toJSON != null + ? addEditCipherInfo.cipher + : CipherView.fromJSON(addEditCipherInfo?.cipher as Jsonify<CipherView>); + + return { cipher, collectionIds: addEditCipherInfo.collectionIds }; + }, + }, +); diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 3e8bd92a7a..ff8e9f1f4f 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -11,8 +11,10 @@ import { OrganizationData } from "../../../admin-console/models/data/organizatio import { PolicyData } from "../../../admin-console/models/data/policy.data"; import { ProviderData } from "../../../admin-console/models/data/provider.data"; import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; +import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -49,6 +51,8 @@ export class SyncService implements SyncServiceAbstraction { syncInProgress = false; constructor( + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private accountService: AccountService, private apiService: ApiService, private domainSettingsService: DomainSettingsService, private folderService: InternalFolderService, @@ -244,7 +248,7 @@ export class SyncService implements SyncServiceAbstraction { this.syncStarted(); if (await this.stateService.getIsAuthenticated()) { try { - const localSend = this.sendService.get(notification.id); + const localSend = await firstValueFrom(this.sendService.get$(notification.id)); if ( (!isEdit && localSend == null) || (isEdit && localSend != null && localSend.revisionDate < notification.revisionDate) @@ -352,8 +356,10 @@ export class SyncService implements SyncServiceAbstraction { private async setForceSetPasswordReasonIfNeeded(profileResponse: ProfileResponse) { // The `forcePasswordReset` flag indicates an admin has reset the user's password and must be updated if (profileResponse.forcePasswordReset) { - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.AdminForcePasswordReset, + userId, ); } @@ -387,8 +393,10 @@ export class SyncService implements SyncServiceAbstraction { ) { // TDE user w/out MP went from having no password reset permission to having it. // Must set the force password reset reason so the auth guard will redirect to the set password page. - await this.stateService.setForceSetPasswordReason( + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, ); } } diff --git a/libs/common/test.setup.ts b/libs/common/test.setup.ts index c50c7ca227..d857751b51 100644 --- a/libs/common/test.setup.ts +++ b/libs/common/test.setup.ts @@ -1,6 +1,7 @@ import { webcrypto } from "crypto"; import { toEqualBuffer } from "./spec"; +import { toAlmostEqual } from "./spec/matchers/to-almost-equal"; Object.defineProperty(window, "crypto", { value: webcrypto, @@ -10,8 +11,15 @@ Object.defineProperty(window, "crypto", { expect.extend({ toEqualBuffer: toEqualBuffer, + toAlmostEqual: toAlmostEqual, }); export interface CustomMatchers<R = unknown> { toEqualBuffer(expected: Uint8Array | ArrayBuffer): R; + /** + * Matches the expected date within an optional ms precision + * @param expected The expected date + * @param msPrecision The optional precision in milliseconds + */ + toAlmostEqual(expected: Date, msPrecision?: number): R; } diff --git a/libs/components/src/avatar/avatar.component.ts b/libs/components/src/avatar/avatar.component.ts index 429e9fc0c6..b19506952d 100644 --- a/libs/components/src/avatar/avatar.component.ts +++ b/libs/components/src/avatar/avatar.component.ts @@ -40,7 +40,7 @@ export class AvatarComponent implements OnChanges { get classList() { return ["tw-rounded-full"] .concat(SizeClasses[this.size] ?? []) - .concat(this.border ? ["tw-border", "tw-border-solid", "tw-border-secondary-500"] : []); + .concat(this.border ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []); } private generate() { diff --git a/libs/components/src/avatar/avatar.mdx b/libs/components/src/avatar/avatar.mdx index c6c5ff78ba..0f3f6f06a9 100644 --- a/libs/components/src/avatar/avatar.mdx +++ b/libs/components/src/avatar/avatar.mdx @@ -44,7 +44,7 @@ Use the user 'ID' field if `Name` is not defined. ## Outline If the avatar is displayed on one of the theme's `background` color variables or is interactive, -display the avatar with a 1 pixel `secondary-500` border to meet WCAG AA graphic contrast guidelines +display the avatar with a 1 pixel `secondary-600` border to meet WCAG AA graphic contrast guidelines for interactive elements. <Story of={stories.Border} /> @@ -64,4 +64,4 @@ When the avatar is used as a button, the following states should be used: ## Accessibility Avatar background color should have 3.1:1 contrast with it’s background; or include the -`secondary-500` border Avatar text should have 4.5:1 contrast with the avatar background color +`secondary-600` border Avatar text should have 4.5:1 contrast with the avatar background color diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index dd28d86ae8..b81b9f80e2 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -3,12 +3,12 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info"; const styles: Record<BadgeVariant, string[]> = { - primary: ["tw-bg-primary-500"], + primary: ["tw-bg-primary-600"], secondary: ["tw-bg-text-muted"], - success: ["tw-bg-success-500"], - danger: ["tw-bg-danger-500"], - warning: ["tw-bg-warning-500"], - info: ["tw-bg-info-500"], + success: ["tw-bg-success-600"], + danger: ["tw-bg-danger-600"], + warning: ["tw-bg-warning-600"], + info: ["tw-bg-info-600"], }; const hoverStyles: Record<BadgeVariant, string[]> = { diff --git a/libs/components/src/banner/banner.component.ts b/libs/components/src/banner/banner.component.ts index e93bcb2214..099fa11fa4 100644 --- a/libs/components/src/banner/banner.component.ts +++ b/libs/components/src/banner/banner.component.ts @@ -28,13 +28,13 @@ export class BannerComponent implements OnInit { get bannerClass() { switch (this.bannerType) { case "danger": - return "tw-bg-danger-500"; + return "tw-bg-danger-600"; case "info": - return "tw-bg-info-500"; + return "tw-bg-info-600"; case "premium": - return "tw-bg-success-500"; + return "tw-bg-success-600"; case "warning": - return "tw-bg-warning-500"; + return "tw-bg-warning-600"; } } } diff --git a/libs/components/src/button/button.component.spec.ts b/libs/components/src/button/button.component.spec.ts index a0b9c1e7a5..a75ac400a9 100644 --- a/libs/components/src/button/button.component.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -30,8 +30,8 @@ describe("Button", () => { it("should apply classes based on type", () => { testAppComponent.buttonType = "primary"; fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-bg-primary-500")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-bg-primary-500")).toBe(true); + expect(buttonDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true); + expect(linkDebugElement.nativeElement.classList.contains("tw-bg-primary-600")).toBe(true); testAppComponent.buttonType = "secondary"; fixture.detectChanges(); @@ -40,8 +40,8 @@ describe("Button", () => { testAppComponent.buttonType = "danger"; fixture.detectChanges(); - expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); - expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-500")).toBe(true); + expect(buttonDebugElement.nativeElement.classList.contains("tw-border-danger-600")).toBe(true); + expect(linkDebugElement.nativeElement.classList.contains("tw-border-danger-600")).toBe(true); testAppComponent.buttonType = "unstyled"; fixture.detectChanges(); diff --git a/libs/components/src/button/button.component.ts b/libs/components/src/button/button.component.ts index 414d4e5913..3cbacb4731 100644 --- a/libs/components/src/button/button.component.ts +++ b/libs/components/src/button/button.component.ts @@ -12,13 +12,13 @@ const focusRing = [ const buttonStyles: Record<ButtonType, string[]> = { primary: [ - "tw-border-primary-500", - "tw-bg-primary-500", + "tw-border-primary-600", + "tw-bg-primary-600", "!tw-text-contrast", "hover:tw-bg-primary-700", "hover:tw-border-primary-700", - "disabled:tw-bg-primary-500/60", - "disabled:tw-border-primary-500/60", + "disabled:tw-bg-primary-600/60", + "disabled:tw-border-primary-600/60", "disabled:!tw-text-contrast/60", "disabled:tw-bg-clip-padding", "disabled:tw-cursor-not-allowed", @@ -39,13 +39,13 @@ const buttonStyles: Record<ButtonType, string[]> = { ], danger: [ "tw-bg-transparent", - "tw-border-danger-500", + "tw-border-danger-600", "!tw-text-danger", - "hover:tw-bg-danger-500", - "hover:tw-border-danger-500", + "hover:tw-bg-danger-600", + "hover:tw-border-danger-600", "hover:!tw-text-contrast", "disabled:tw-bg-transparent", - "disabled:tw-border-danger-500/60", + "disabled:tw-border-danger-600/60", "disabled:!tw-text-danger/60", "disabled:tw-cursor-not-allowed", ...focusRing, diff --git a/libs/components/src/callout/callout.component.ts b/libs/components/src/callout/callout.component.ts index 7ce79071bf..6942d4bc15 100644 --- a/libs/components/src/callout/callout.component.ts +++ b/libs/components/src/callout/callout.component.ts @@ -42,13 +42,13 @@ export class CalloutComponent implements OnInit { get calloutClass() { switch (this.type) { case "danger": - return "tw-border-l-danger-500"; + return "tw-border-l-danger-600"; case "info": - return "tw-border-l-info-500"; + return "tw-border-l-info-600"; case "success": - return "tw-border-l-success-500"; + return "tw-border-l-success-600"; case "warning": - return "tw-border-l-warning-500"; + return "tw-border-l-warning-600"; } } diff --git a/libs/components/src/checkbox/checkbox.component.ts b/libs/components/src/checkbox/checkbox.component.ts index bbc288659c..d8fd3f76ea 100644 --- a/libs/components/src/checkbox/checkbox.component.ts +++ b/libs/components/src/checkbox/checkbox.component.ts @@ -20,7 +20,7 @@ export class CheckboxComponent implements BitFormControlAbstraction { "tw-rounded", "tw-border", "tw-border-solid", - "tw-border-secondary-500", + "tw-border-secondary-600", "tw-h-3.5", "tw-w-3.5", "tw-mr-1.5", @@ -43,8 +43,8 @@ export class CheckboxComponent implements BitFormControlAbstraction { "disabled:tw-border", "disabled:tw-bg-secondary-100", - "checked:tw-bg-primary-500", - "checked:tw-border-primary-500", + "checked:tw-bg-primary-600", + "checked:tw-border-primary-600", "checked:hover:tw-bg-primary-700", "checked:hover:tw-border-primary-700", "[&>label:hover]:checked:tw-bg-primary-700", @@ -59,8 +59,8 @@ export class CheckboxComponent implements BitFormControlAbstraction { "[&:not(:indeterminate)]:checked:before:tw-mask-image-[var(--mask-image)]", "indeterminate:before:tw-mask-image-[var(--indeterminate-mask-image)]", - "indeterminate:tw-bg-primary-500", - "indeterminate:tw-border-primary-500", + "indeterminate:tw-bg-primary-600", + "indeterminate:tw-border-primary-600", "indeterminate:hover:tw-bg-primary-700", "indeterminate:hover:tw-border-primary-700", "[&>label:hover]:indeterminate:tw-bg-primary-700", diff --git a/libs/components/src/color-password/color-password.component.ts b/libs/components/src/color-password/color-password.component.ts index 4c32d0af0d..efa2ab687f 100644 --- a/libs/components/src/color-password/color-password.component.ts +++ b/libs/components/src/color-password/color-password.component.ts @@ -30,7 +30,7 @@ export class ColorPasswordComponent { [CharacterType.Emoji]: [], [CharacterType.Letter]: ["tw-text-main"], [CharacterType.Special]: ["tw-text-danger"], - [CharacterType.Number]: ["tw-text-primary-500"], + [CharacterType.Number]: ["tw-text-primary-600"], }; @HostBinding("class") diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts index e93498ec48..1438c7926e 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.component.ts @@ -15,7 +15,7 @@ const DEFAULT_ICON: Record<SimpleDialogType, string> = { }; const DEFAULT_COLOR: Record<SimpleDialogType, string> = { - primary: "tw-text-primary-500", + primary: "tw-text-primary-600", success: "tw-text-success", info: "tw-text-info", warning: "tw-text-warning", diff --git a/libs/components/src/dialog/simple-dialog/simple-dialog.mdx b/libs/components/src/dialog/simple-dialog/simple-dialog.mdx index 5e45b7bef6..a78ba4650a 100644 --- a/libs/components/src/dialog/simple-dialog/simple-dialog.mdx +++ b/libs/components/src/dialog/simple-dialog/simple-dialog.mdx @@ -33,11 +33,11 @@ the simple dialog's type is specified. | type | icon name | icon | color | | ------- | ------------------------ | -------------------------------------------- | ----------- | -| primary | bwi-business | <i class="bwi bwi-business"></i> | primary-500 | -| success | bwi-star | <i class="bwi bwi-star"></i> | success-500 | -| info | bwi-info-circle | <i class="bwi bwi-info-circle"></i> | info-500 | -| warning | bwi-exclamation-triangle | <i class="bwi bwi-exclamation-triangle"></i> | warning-500 | -| danger | bwi-error | <i class="bwi bwi-error"></i> | danger-500 | +| primary | bwi-business | <i class="bwi bwi-business"></i> | primary-600 | +| success | bwi-star | <i class="bwi bwi-star"></i> | success-600 | +| info | bwi-info-circle | <i class="bwi bwi-info-circle"></i> | info-600 | +| warning | bwi-exclamation-triangle | <i class="bwi bwi-exclamation-triangle"></i> | warning-600 | +| danger | bwi-error | <i class="bwi bwi-error"></i> | danger-600 | ## Scrolling Content diff --git a/libs/components/src/form-field/prefix.directive.ts b/libs/components/src/form-field/prefix.directive.ts index 62643c8bb7..6e1e15fd20 100644 --- a/libs/components/src/form-field/prefix.directive.ts +++ b/libs/components/src/form-field/prefix.directive.ts @@ -6,7 +6,7 @@ export const PrefixClasses = [ "tw-bg-background-alt", "tw-border", "tw-border-solid", - "tw-border-secondary-500", + "tw-border-secondary-600", "tw-text-muted", "tw-rounded-none", ]; diff --git a/libs/components/src/form/forms.mdx b/libs/components/src/form/forms.mdx index 0845156561..a42ddccbe6 100644 --- a/libs/components/src/form/forms.mdx +++ b/libs/components/src/form/forms.mdx @@ -174,5 +174,5 @@ the field’s label. - All field inputs are interactive elements that must follow the WCAG graphic contrast guidelines. Maintain a ratio of 3:1 with the form's background. -- Error styling should not rely only on using the `danger-500`color change. Use +- Error styling should not rely only on using the `danger-600`color change. Use <i class="bwi bwi-error"></i> as a prefix to highlight the text as error text versus helper diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 73872926f8..53e8032795 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -60,15 +60,15 @@ const styles: Record<IconButtonType, string[]> = { ...focusRing, ], primary: [ - "tw-bg-primary-500", + "tw-bg-primary-600", "!tw-text-contrast", - "tw-border-primary-500", + "tw-border-primary-600", "hover:tw-bg-primary-700", "hover:tw-border-primary-700", "focus-visible:before:tw-ring-primary-700", "disabled:tw-opacity-60", - "disabled:hover:tw-border-primary-500", - "disabled:hover:tw-bg-primary-500", + "disabled:hover:tw-border-primary-600", + "disabled:hover:tw-bg-primary-600", ...focusRing, ], secondary: [ @@ -88,15 +88,15 @@ const styles: Record<IconButtonType, string[]> = { danger: [ "tw-bg-transparent", "!tw-text-danger", - "tw-border-danger-500", + "tw-border-danger-600", "hover:!tw-text-contrast", - "hover:tw-bg-danger-500", + "hover:tw-bg-danger-600", "focus-visible:before:tw-ring-primary-700", "disabled:tw-opacity-60", - "disabled:hover:tw-border-danger-500", + "disabled:hover:tw-border-danger-600", "disabled:hover:tw-bg-transparent", "disabled:hover:!tw-text-danger", - "disabled:hover:tw-border-danger-500", + "disabled:hover:tw-border-danger-600", ...focusRing, ], light: [ diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index 19bc972a70..0f25d2de58 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -30,7 +30,7 @@ export const Default: Story = { <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="primary" [size]="size">Button</button> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="secondary"[size]="size">Button</button> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="danger" [size]="size">Button</button> - <div class="tw-bg-primary-500 tw-p-2 tw-inline-block"> + <div class="tw-bg-primary-600 tw-p-2 tw-inline-block"> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="contrast" [size]="size">Button</button> </div> <div class="tw-bg-background-alt2 tw-p-2 tw-inline-block"> @@ -111,7 +111,7 @@ export const Contrast: Story = { render: (args) => ({ props: args, template: ` - <div class="tw-bg-primary-500 tw-p-6 tw-w-full tw-inline-block"> + <div class="tw-bg-primary-600 tw-p-6 tw-w-full tw-inline-block"> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button> </div> `, diff --git a/libs/components/src/icon/icon.stories.ts b/libs/components/src/icon/icon.stories.ts index 95bf457517..54cdd7928c 100644 --- a/libs/components/src/icon/icon.stories.ts +++ b/libs/components/src/icon/icon.stories.ts @@ -1,31 +1,24 @@ import { Meta, StoryObj } from "@storybook/angular"; import { BitIconComponent } from "./icon.component"; +import * as GenericIcons from "./icons"; export default { title: "Component Library/Icon", component: BitIconComponent, - args: { - icon: "reportExposedPasswords", - }, } as Meta; type Story = StoryObj<BitIconComponent>; -export const ReportExposedPasswords: Story = { - render: (args) => ({ - props: args, - template: ` - <div class="tw-bg-primary-500 tw-p-5"> - <bit-icon [icon]="icon" class="tw-text-primary-300"></bit-icon> - </div> - `, - }), -}; - -export const UnknownIcon: Story = { - ...ReportExposedPasswords, +export const Default: Story = { args: { - icon: "unknown" as any, + icon: GenericIcons.NoAccess, + }, + argTypes: { + icon: { + options: Object.keys(GenericIcons), + mapping: GenericIcons, + control: { type: "select" }, + }, }, }; diff --git a/libs/components/src/icon/icons/no-access.ts b/libs/components/src/icon/icons/no-access.ts index f9ad048752..1011b3089c 100644 --- a/libs/components/src/icon/icons/no-access.ts +++ b/libs/components/src/icon/icons/no-access.ts @@ -2,11 +2,11 @@ import { svgIcon } from "../icon"; export const NoAccess = svgIcon` <svg xmlns="http://www.w3.org/2000/svg" width="154" height="130" fill="none"> - <path class="tw-stroke-secondary-500" d="M60.795 112.1h55.135a4 4 0 0 0 4-4V59.65M32.9 51.766V6a4 4 0 0 1 4-4h79.03a4 4 0 0 1 4 4v19.992" stroke-width="4"/> - <path class="tw-stroke-secondary-500" d="M46.997 21.222h13.806M69.832 21.222h13.806M93.546 21.222h13.806M46.997 44.188h13.806M69.832 44.188h13.806M93.546 44.188h13.806M50.05 67.02h10.753M69.832 67.02h13.806M93.546 67.02h13.806M46.997 90.118h13.806M69.832 90.118h13.806M93.546 90.118h13.806" stroke-width="2" stroke-linecap="round"/> - <path class="tw-stroke-secondary-500" d="M30.914 89.366c10.477 0 18.97-8.493 18.97-18.97 0-10.476-8.493-18.97-18.97-18.97-10.476 0-18.969 8.494-18.969 18.97 0 10.477 8.493 18.97 18.97 18.97ZM2.313 117.279c2.183-16.217 15.44-27.362 29.623-27.362 14.07 0 25.942 11.022 27.898 27.33.167 1.39-.988 2.753-2.719 2.753H5c-1.741 0-2.87-1.366-2.687-2.721Z" stroke-width="4"/> - <path class="tw-stroke-danger-500" d="m147.884 50.361-15.89-27.522c-2.31-4-8.083-4-10.392 0l-15.891 27.523c-2.309 4 .578 9 5.196 9h31.781c4.619 0 7.505-5 5.196-9Z" stroke-width="4"/> - <path class="tw-stroke-danger-500" d="M126.798 29.406v16.066" stroke-width="4" stroke-linecap="round"/> - <path class="tw-fill-danger-500" d="M126.798 54.727a2.635 2.635 0 1 0 0-5.27 2.635 2.635 0 0 0 0 5.27Z" /> + <path class="tw-stroke-secondary-600" d="M60.795 112.1h55.135a4 4 0 0 0 4-4V59.65M32.9 51.766V6a4 4 0 0 1 4-4h79.03a4 4 0 0 1 4 4v19.992" stroke-width="4"/> + <path class="tw-stroke-secondary-600" d="M46.997 21.222h13.806M69.832 21.222h13.806M93.546 21.222h13.806M46.997 44.188h13.806M69.832 44.188h13.806M93.546 44.188h13.806M50.05 67.02h10.753M69.832 67.02h13.806M93.546 67.02h13.806M46.997 90.118h13.806M69.832 90.118h13.806M93.546 90.118h13.806" stroke-width="2" stroke-linecap="round"/> + <path class="tw-stroke-secondary-600" d="M30.914 89.366c10.477 0 18.97-8.493 18.97-18.97 0-10.476-8.493-18.97-18.97-18.97-10.476 0-18.969 8.494-18.969 18.97 0 10.477 8.493 18.97 18.97 18.97ZM2.313 117.279c2.183-16.217 15.44-27.362 29.623-27.362 14.07 0 25.942 11.022 27.898 27.33.167 1.39-.988 2.753-2.719 2.753H5c-1.741 0-2.87-1.366-2.687-2.721Z" stroke-width="4"/> + <path class="tw-stroke-danger-600" d="m147.884 50.361-15.89-27.522c-2.31-4-8.083-4-10.392 0l-15.891 27.523c-2.309 4 .578 9 5.196 9h31.781c4.619 0 7.505-5 5.196-9Z" stroke-width="4"/> + <path class="tw-stroke-danger-600" d="M126.798 29.406v16.066" stroke-width="4" stroke-linecap="round"/> + <path class="tw-fill-danger-600" d="M126.798 54.727a2.635 2.635 0 1 0 0-5.27 2.635 2.635 0 0 0 0 5.27Z" /> </svg> `; diff --git a/libs/components/src/icon/icons/search.ts b/libs/components/src/icon/icons/search.ts index de41dd3b19..914fa0e981 100644 --- a/libs/components/src/icon/icons/search.ts +++ b/libs/components/src/icon/icons/search.ts @@ -4,15 +4,15 @@ export const Search = svgIcon` <svg width="120" height="120" fill="none" xmlns="http://www.w3.org/2000/svg"> <g opacity=".49"> <path class="tw-fill-secondary-300" fill-rule="evenodd" clip-rule="evenodd" d="M40.36 73.256a30.004 30.004 0 0 0 10.346 1.826c16.282 0 29.482-12.912 29.482-28.84 0-.384-.008-.766-.023-1.145h28.726v39.57H40.36v-11.41Z" /> - <path class="tw-stroke-secondary-500" d="M21.546 46.241c0 15.929 13.2 28.841 29.482 28.841S80.51 62.17 80.51 46.241c0-15.928-13.2-28.841-29.482-28.841S21.546 30.313 21.546 46.241Z" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" /> - <path class="tw-fill-secondary-500" d="M35.36 70.595a1.2 1.2 0 0 0-2.4 0h2.4Zm77.475-30.356a2.343 2.343 0 0 1 2.365 2.33h2.4c0-2.593-2.107-4.73-4.765-4.73v2.4Zm2.365 2.33v46.047h2.4V42.57h-2.4Zm0 46.047c0 1.293-1.058 2.33-2.365 2.33v2.4c2.59 0 4.765-2.069 4.765-4.73h-2.4Zm-2.365 2.33h-75.11v2.4h75.11v-2.4Zm-75.11 0a2.343 2.343 0 0 1-2.365-2.33h-2.4c0 2.594 2.107 4.73 4.766 4.73v-2.4Zm-2.365-2.33v-18.02h-2.4v18.02h2.4Zm44.508-48.377h32.967v-2.4H79.868v2.4Z" /> - <path class="tw-stroke-secondary-500" d="M79.907 45.287h29.114v39.57H40.487V73.051" stroke-width="2" stroke-linejoin="round" /> - <path class="tw-stroke-secondary-500" d="M57.356 102.56h35.849" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" /> - <path class="tw-stroke-secondary-500" d="M68.954 92.147v10.413m11.599-10.413v10.413" stroke-width="4" stroke-linejoin="round" /> - <path class="tw-stroke-secondary-500" d="m27.44 64.945-4.51 4.51L5.72 86.663a3 3 0 0 0 0 4.243l1.238 1.238a3 3 0 0 0 4.243 0L28.41 74.936l4.51-4.51" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" /> - <path class="tw-stroke-secondary-500" d="M101.293 53.154H85.178m16.115 6.043H90.214m-5.036 0h-7.553m23.668 6.043h-7.05m-5.54 0h-15.61m28.2 6.042H85.178m-5.538 0h-8.562m30.215 6.043H78.632m-5.539 0H60m-5.54 0h-8.057" stroke-width="2" stroke-linecap="round" /> - <path class="tw-stroke-secondary-500" d="M29.164 33.01h41.529a2.4 2.4 0 0 1 2.4 2.4v6.28a2.4 2.4 0 0 1-2.4 2.4h-41.53a2.4 2.4 0 0 1-2.4-2.4v-6.28a2.4 2.4 0 0 1 2.4-2.4Z" stroke-width="4" /> - <path class="tw-stroke-secondary-500" d="M22.735 54.16h34.361a2.4 2.4 0 0 1 2.4 2.4v6.28a2.4 2.4 0 0 1-2.4 2.4H28.778m50.358-11.08h-6.161a2.4 2.4 0 0 0-2.4 2.4v6.414a2.266 2.266 0 0 0 2.266 2.265" stroke-width="4" stroke-linecap="round" /> + <path class="tw-stroke-secondary-600" d="M21.546 46.241c0 15.929 13.2 28.841 29.482 28.841S80.51 62.17 80.51 46.241c0-15.928-13.2-28.841-29.482-28.841S21.546 30.313 21.546 46.241Z" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" /> + <path class="tw-fill-secondary-600" d="M35.36 70.595a1.2 1.2 0 0 0-2.4 0h2.4Zm77.475-30.356a2.343 2.343 0 0 1 2.365 2.33h2.4c0-2.593-2.107-4.73-4.765-4.73v2.4Zm2.365 2.33v46.047h2.4V42.57h-2.4Zm0 46.047c0 1.293-1.058 2.33-2.365 2.33v2.4c2.59 0 4.765-2.069 4.765-4.73h-2.4Zm-2.365 2.33h-75.11v2.4h75.11v-2.4Zm-75.11 0a2.343 2.343 0 0 1-2.365-2.33h-2.4c0 2.594 2.107 4.73 4.766 4.73v-2.4Zm-2.365-2.33v-18.02h-2.4v18.02h2.4Zm44.508-48.377h32.967v-2.4H79.868v2.4Z" /> + <path class="tw-stroke-secondary-600" d="M79.907 45.287h29.114v39.57H40.487V73.051" stroke-width="2" stroke-linejoin="round" /> + <path class="tw-stroke-secondary-600" d="M57.356 102.56h35.849" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" /> + <path class="tw-stroke-secondary-600" d="M68.954 92.147v10.413m11.599-10.413v10.413" stroke-width="4" stroke-linejoin="round" /> + <path class="tw-stroke-secondary-600" d="m27.44 64.945-4.51 4.51L5.72 86.663a3 3 0 0 0 0 4.243l1.238 1.238a3 3 0 0 0 4.243 0L28.41 74.936l4.51-4.51" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" /> + <path class="tw-stroke-secondary-600" d="M101.293 53.154H85.178m16.115 6.043H90.214m-5.036 0h-7.553m23.668 6.043h-7.05m-5.54 0h-15.61m28.2 6.042H85.178m-5.538 0h-8.562m30.215 6.043H78.632m-5.539 0H60m-5.54 0h-8.057" stroke-width="2" stroke-linecap="round" /> + <path class="tw-stroke-secondary-600" d="M29.164 33.01h41.529a2.4 2.4 0 0 1 2.4 2.4v6.28a2.4 2.4 0 0 1-2.4 2.4h-41.53a2.4 2.4 0 0 1-2.4-2.4v-6.28a2.4 2.4 0 0 1 2.4-2.4Z" stroke-width="4" /> + <path class="tw-stroke-secondary-600" d="M22.735 54.16h34.361a2.4 2.4 0 0 1 2.4 2.4v6.28a2.4 2.4 0 0 1-2.4 2.4H28.778m50.358-11.08h-6.161a2.4 2.4 0 0 0-2.4 2.4v6.414a2.266 2.266 0 0 0 2.266 2.265" stroke-width="4" stroke-linecap="round" /> </g> </svg> `; diff --git a/libs/components/src/input/input.directive.ts b/libs/components/src/input/input.directive.ts index 9bd110704e..27c7d8175d 100644 --- a/libs/components/src/input/input.directive.ts +++ b/libs/components/src/input/input.directive.ts @@ -29,7 +29,7 @@ export class BitInputDirective implements BitFormFieldControl { "tw-bg-background-alt", "tw-border", "tw-border-solid", - this.hasError ? "tw-border-danger-500" : "tw-border-secondary-500", + this.hasError ? "tw-border-danger-600" : "tw-border-secondary-600", "tw-text-main", "tw-placeholder-text-muted", // Rounded diff --git a/libs/components/src/link/link.directive.ts b/libs/components/src/link/link.directive.ts index 6d923acf3d..a8ee528da1 100644 --- a/libs/components/src/link/link.directive.ts +++ b/libs/components/src/link/link.directive.ts @@ -4,10 +4,10 @@ export type LinkType = "primary" | "secondary" | "contrast" | "light"; const linkStyles: Record<LinkType, string[]> = { primary: [ - "!tw-text-primary-500", - "hover:!tw-text-primary-500", + "!tw-text-primary-600", + "hover:!tw-text-primary-600", "focus-visible:before:tw-ring-primary-700", - "disabled:!tw-text-primary-500/60", + "disabled:!tw-text-primary-600/60", ], secondary: [ "!tw-text-main", diff --git a/libs/components/src/link/link.mdx b/libs/components/src/link/link.mdx index 48c8c2abd5..100824277a 100644 --- a/libs/components/src/link/link.mdx +++ b/libs/components/src/link/link.mdx @@ -6,7 +6,7 @@ import * as stories from "./link.stories"; # Link / Text button -Text Links and Buttons use the `primary-500` color and can use either the `<a>` or `<button>` tags. +Text Links and Buttons use the `primary-600` color and can use either the `<a>` or `<button>` tags. Choose which based on the action the button takes: - if navigating to a new page, use a `<a>` diff --git a/libs/components/src/link/link.stories.ts b/libs/components/src/link/link.stories.ts index 76bb4d4752..86feb80cc1 100644 --- a/libs/components/src/link/link.stories.ts +++ b/libs/components/src/link/link.stories.ts @@ -30,7 +30,7 @@ export const Buttons: Story = { render: (args) => ({ props: args, template: ` - <div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }"> + <div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-600': linkType === 'contrast' }"> <div class="tw-block tw-p-2"> <button bitLink [linkType]="linkType">Button</button> </div> @@ -61,7 +61,7 @@ export const Anchors: StoryObj<AnchorLinkDirective> = { render: (args) => ({ props: args, template: ` - <div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-500': linkType === 'contrast' }"> + <div class="tw-p-2" [ngClass]="{ 'tw-bg-transparent': linkType != 'contrast', 'tw-bg-primary-600': linkType === 'contrast' }"> <div class="tw-block tw-p-2"> <a bitLink [linkType]="linkType" href="#">Anchor</a> </div> @@ -108,7 +108,7 @@ export const Disabled: Story = { template: ` <button bitLink disabled linkType="primary" class="tw-mr-2">Primary</button> <button bitLink disabled linkType="secondary" class="tw-mr-2">Secondary</button> - <div class="tw-bg-primary-500 tw-p-2 tw-inline-block"> + <div class="tw-bg-primary-600 tw-p-2 tw-inline-block"> <button bitLink disabled linkType="contrast" class="tw-mr-2">Contrast</button> </div> `, diff --git a/libs/components/src/menu/menu-divider.component.html b/libs/components/src/menu/menu-divider.component.html index c7c2c739d2..7d9fee3e8f 100644 --- a/libs/components/src/menu/menu-divider.component.html +++ b/libs/components/src/menu/menu-divider.component.html @@ -1,4 +1,5 @@ <div - class="tw-my-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-secondary-500" + class="tw-my-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-secondary-600" role="separator" + aria-hidden="true" ></div> diff --git a/libs/components/src/menu/menu-item.directive.ts b/libs/components/src/menu/menu-item.directive.ts index 2a50dd366f..77246bbcdf 100644 --- a/libs/components/src/menu/menu-item.directive.ts +++ b/libs/components/src/menu/menu-item.directive.ts @@ -16,12 +16,12 @@ export class MenuItemDirective implements FocusableOption { "tw-bg-background", "tw-text-left", "hover:tw-bg-secondary-100", - "focus:tw-bg-secondary-100", - "focus:tw-z-50", - "focus:tw-outline-none", - "focus:tw-ring", - "focus:tw-ring-offset-2", - "focus:tw-ring-primary-700", + "focus-visible:tw-bg-secondary-100", + "focus-visible:tw-z-50", + "focus-visible:tw-outline-none", + "focus-visible:tw-ring", + "focus-visible:tw-ring-offset-2", + "focus-visible:tw-ring-primary-700", "active:!tw-ring-0", "active:!tw-ring-offset-0", ]; diff --git a/libs/components/src/menu/menu-trigger-for.directive.ts b/libs/components/src/menu/menu-trigger-for.directive.ts index 20ae0b1ce7..05f2e7a8ef 100644 --- a/libs/components/src/menu/menu-trigger-for.directive.ts +++ b/libs/components/src/menu/menu-trigger-for.directive.ts @@ -88,11 +88,12 @@ export class MenuTriggerForDirective implements OnDestroy { } this.destroyMenu(); }); - this.keyDownEventsSub = - this.menu.keyManager && - this.overlayRef + if (this.menu.keyManager) { + this.menu.keyManager.setFirstItemActive(); + this.keyDownEventsSub = this.overlayRef .keydownEvents() .subscribe((event: KeyboardEvent) => this.menu.keyManager.onKeydown(event)); + } } private destroyMenu() { diff --git a/libs/components/src/menu/menu.component.html b/libs/components/src/menu/menu.component.html index 98a35e97de..5b6b15b5cb 100644 --- a/libs/components/src/menu/menu.component.html +++ b/libs/components/src/menu/menu.component.html @@ -1,7 +1,7 @@ <ng-template> <div (click)="closed.emit()" - class="tw-flex tw-shrink-0 tw-flex-col tw-rounded tw-border tw-border-solid tw-border-secondary-500 tw-bg-background tw-bg-clip-padding tw-py-2 tw-overflow-y-auto" + class="tw-flex tw-shrink-0 tw-flex-col tw-rounded tw-border tw-border-solid tw-border-secondary-600 tw-bg-background tw-bg-clip-padding tw-py-2 tw-overflow-y-auto" [attr.role]="ariaRole" [attr.aria-label]="ariaLabel" cdkTrapFocus diff --git a/libs/components/src/multi-select/multi-select.component.ts b/libs/components/src/multi-select/multi-select.component.ts index 42d53445d6..b0a2cf613b 100644 --- a/libs/components/src/multi-select/multi-select.component.ts +++ b/libs/components/src/multi-select/multi-select.component.ts @@ -42,7 +42,7 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro @Input() disabled = false; // Internal tracking of selected items - @Input() selectedItems: SelectItemView[]; + protected selectedItems: SelectItemView[]; // Default values for our implementation loadingText: string; diff --git a/libs/components/src/multi-select/scss/bw.theme.scss b/libs/components/src/multi-select/scss/bw.theme.scss index 234ea82019..b567c72592 100644 --- a/libs/components/src/multi-select/scss/bw.theme.scss +++ b/libs/components/src/multi-select/scss/bw.theme.scss @@ -8,7 +8,7 @@ $ng-select-highlight: rgb(var(--color-primary-700)) !default; $ng-select-primary-text: rgb(var(--color-text-main)) !default; $ng-select-disabled-text: rgb(var(--color-secondary-100)) !default; -$ng-select-border: rgb(var(--color-secondary-500)) !default; +$ng-select-border: rgb(var(--color-secondary-600)) !default; $ng-select-border-radius: 4px !default; $ng-select-bg: rgb(var(--color-background-alt)) !default; $ng-select-selected: transparent !default; diff --git a/libs/components/src/popover/popover.stories.ts b/libs/components/src/popover/popover.stories.ts index 6e7d352471..10b7b248b7 100644 --- a/libs/components/src/popover/popover.stories.ts +++ b/libs/components/src/popover/popover.stories.ts @@ -78,7 +78,7 @@ export const Default: Story = { <div class="tw-mt-32"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" > @@ -119,7 +119,7 @@ export const InitiallyOpen: Story = { <div class="tw-mt-32"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" [popoverOpen]="true" #triggerRef="popoverTrigger" @@ -145,7 +145,7 @@ export const RightStart: Story = { <div class="tw-mt-32"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -168,7 +168,7 @@ export const RightCenter: Story = { <div class="tw-mt-32"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -191,7 +191,7 @@ export const RightEnd: Story = { <div class="tw-mt-32"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -214,7 +214,7 @@ export const LeftStart: Story = { <div class="tw-mt-32 tw-flex tw-justify-end"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -237,7 +237,7 @@ export const LeftCenter: Story = { <div class="tw-mt-32 tw-flex tw-justify-end"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -259,7 +259,7 @@ export const LeftEnd: Story = { <div class="tw-mt-32 tw-flex tw-justify-end"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -282,7 +282,7 @@ export const BelowStart: Story = { <div class="tw-mt-32 tw-flex tw-justify-center"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -305,7 +305,7 @@ export const BelowCenter: Story = { <div class="tw-mt-32 tw-flex tw-justify-center"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -328,7 +328,7 @@ export const BelowEnd: Story = { <div class="tw-mt-32 tw-flex tw-justify-center"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -351,7 +351,7 @@ export const AboveStart: Story = { <div class="tw-mt-32 tw-flex tw-justify-center"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -374,7 +374,7 @@ export const AboveCenter: Story = { <div class="tw-mt-32 tw-flex tw-justify-center"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" @@ -397,7 +397,7 @@ export const AboveEnd: Story = { <div class="tw-mt-32 tw-flex tw-justify-center"> <button type="button" - class="tw-border-none tw-bg-transparent tw-text-primary-500" + class="tw-border-none tw-bg-transparent tw-text-primary-600" [bitPopoverTriggerFor]="myPopover" #triggerRef="popoverTrigger" [position]="'${args.position}'" diff --git a/libs/components/src/progress/progress.component.ts b/libs/components/src/progress/progress.component.ts index b166a86f7f..37206dc6ae 100644 --- a/libs/components/src/progress/progress.component.ts +++ b/libs/components/src/progress/progress.component.ts @@ -10,10 +10,10 @@ const SizeClasses: Record<SizeTypes, string[]> = { }; const BackgroundClasses: Record<BackgroundTypes, string[]> = { - danger: ["tw-bg-danger-500"], - primary: ["tw-bg-primary-500"], - success: ["tw-bg-success-500"], - warning: ["tw-bg-warning-500"], + danger: ["tw-bg-danger-600"], + primary: ["tw-bg-primary-600"], + success: ["tw-bg-success-600"], + warning: ["tw-bg-warning-600"], }; @Component({ diff --git a/libs/components/src/progress/progress.mdx b/libs/components/src/progress/progress.mdx index 4959bb91e6..185d6214f1 100644 --- a/libs/components/src/progress/progress.mdx +++ b/libs/components/src/progress/progress.mdx @@ -32,10 +32,10 @@ context of the implementation. For a strength indicator use the following styles for fill: -- **Weak:** `danger-500` -- **Weak2:** `warning-500` -- **Good:** `primary-500` -- **Strong:** `success-500` +- **Weak:** `danger-600` +- **Weak2:** `warning-600` +- **Good:** `primary-600` +- **Strong:** `success-600` ## Accessibility diff --git a/libs/components/src/radio-button/radio-input.component.ts b/libs/components/src/radio-button/radio-input.component.ts index 8b36cbf5ed..df549a751b 100644 --- a/libs/components/src/radio-button/radio-input.component.ts +++ b/libs/components/src/radio-button/radio-input.component.ts @@ -24,7 +24,7 @@ export class RadioInputComponent implements BitFormControlAbstraction { "tw-rounded-full", "tw-border", "tw-border-solid", - "tw-border-secondary-500", + "tw-border-secondary-600", "tw-w-3.5", "tw-h-3.5", "tw-mr-1.5", @@ -50,7 +50,7 @@ export class RadioInputComponent implements BitFormControlAbstraction { "disabled:tw-bg-secondary-100", "checked:tw-bg-text-contrast", - "checked:tw-border-primary-500", + "checked:tw-border-primary-600", "checked:hover:tw-border", "checked:hover:tw-border-primary-700", @@ -58,7 +58,7 @@ export class RadioInputComponent implements BitFormControlAbstraction { "[&>label:hover]:checked:tw-bg-primary-700", "[&>label:hover]:checked:tw-border-primary-700", - "checked:before:tw-bg-primary-500", + "checked:before:tw-bg-primary-600", "checked:disabled:tw-border-secondary-100", "checked:disabled:tw-bg-secondary-100", diff --git a/libs/components/src/stories/colors.mdx b/libs/components/src/stories/colors.mdx index 4fe5ad12ce..f6b205be49 100644 --- a/libs/components/src/stories/colors.mdx +++ b/libs/components/src/stories/colors.mdx @@ -26,29 +26,29 @@ export const Table = (args) => ( </tbody> <tbody> {Row("primary-300")} - {Row("primary-500")} + {Row("primary-600")} {Row("primary-700")} </tbody> <tbody> {Row("secondary-100")} {Row("secondary-300")} - {Row("secondary-500")} + {Row("secondary-600")} {Row("secondary-700")} </tbody> <tbody> - {Row("success-500")} + {Row("success-600")} {Row("success-700")} </tbody> <tbody> - {Row("danger-500")} + {Row("danger-600")} {Row("danger-700")} </tbody> <tbody> - {Row("warning-500")} + {Row("warning-600")} {Row("warning-700")} </tbody> <tbody> - {Row("info-500")} + {Row("info-600")} {Row("info-700")} </tbody> <thead> diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts new file mode 100644 index 0000000000..fa3a915f22 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-form.component.ts @@ -0,0 +1,176 @@ +import { Component } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { DialogService } from "../../../dialog"; +import { I18nMockService } from "../../../utils/i18n-mock.service"; +import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; + +@Component({ + standalone: true, + selector: "bit-kitchen-sink-form", + imports: [KitchenSinkSharedModule], + providers: [ + DialogService, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + checkboxRequired: "Option is required", + fieldsNeedAttention: "__$1__ field(s) above need your attention.", + inputEmail: "Input is not an email-address.", + inputMaxValue: (max) => `Input value must not exceed ${max}.`, + inputMinValue: (min) => `Input value must be at least ${min}.`, + inputRequired: "Input is required.", + multiSelectClearAll: "Clear all", + multiSelectLoading: "Retrieving options...", + multiSelectNotFound: "No items found", + multiSelectPlaceholder: "-- Type to Filter --", + required: "required", + selectPlaceholder: "-- Select --", + toggleVisibility: "Toggle visibility", + }); + }, + }, + ], + template: ` + <form [formGroup]="formObj" [bitSubmit]="submit"> + <div class="tw-mb-6"> + <bit-progress [barWidth]="50"></bit-progress> + </div> + + <bit-form-field> + <bit-label>Your favorite feature</bit-label> + <input bitInput formControlName="favFeature" /> + </bit-form-field> + + <bit-form-field> + <bit-label>Your favorite color</bit-label> + <bit-select formControlName="favColor"> + <bit-option + *ngFor="let color of colors" + [value]="color.value" + [label]="color.name" + ></bit-option> + </bit-select> + </bit-form-field> + + <bit-form-field> + <bit-label>Your top 3 worst passwords</bit-label> + <bit-multi-select formControlName="topWorstPasswords" [baseItems]="worstPasswords"> + </bit-multi-select> + </bit-form-field> + + <bit-form-field> + <bit-label>How many passwords do you have?</bit-label> + <input bitInput type="number" formControlName="numPasswords" min="0" max="150" /> + </bit-form-field> + + <bit-form-field> + <bit-label> + A random password + <button + bitLink + linkType="primary" + [bitPopoverTriggerFor]="myPopover" + #triggerRef="popoverTrigger" + > + <i class="bwi bwi-question-circle"></i> + </button> + </bit-label> + <input bitInput type="password" formControlName="password" /> + <button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button> + </bit-form-field> + + <div class="tw-mb-6"> + <span bitTypography="body1" class="tw-text-main"> + An example of a strong password: &nbsp; + </span> + + <bit-color-password + class="tw-text-base" + [password]="'Wq$Jk😀7j DX#rS5Sdi!z'" + [showCount]="true" + ></bit-color-password> + </div> + + <bit-form-control> + <bit-label>Check if you love security</bit-label> + <input type="checkbox" bitCheckbox formControlName="loveSecurity" /> + <bit-hint>Hint: the correct answer is yes!</bit-hint> + </bit-form-control> + + <bit-radio-group formControlName="current"> + <bit-label>Do you currently use Bitwarden?</bit-label> + <bit-radio-button value="yes"> + <bit-label>Yes</bit-label> + </bit-radio-button> + <bit-radio-button value="no"> + <bit-label>No</bit-label> + </bit-radio-button> + </bit-radio-group> + + <button bitButton bitFormButton buttonType="primary" type="submit">Submit</button> + <bit-error-summary [formGroup]="formObj"></bit-error-summary> + + <bit-popover [title]="'Password help'" #myPopover> + <div>A strong password has the following:</div> + <ul class="tw-mt-2 tw-mb-0 tw-pl-4"> + <li>Letters</li> + <li>Numbers</li> + <li>Special characters</li> + </ul> + </bit-popover> + </form> + `, +}) +export class KitchenSinkForm { + constructor( + public dialogService: DialogService, + public formBuilder: FormBuilder, + ) {} + + formObj = this.formBuilder.group({ + favFeature: ["", [Validators.required]], + favColor: [undefined as string | undefined, [Validators.required]], + topWorstPasswords: [undefined as string | undefined], + loveSecurity: [false, [Validators.requiredTrue]], + current: ["yes"], + numPasswords: [null, [Validators.min(0), Validators.max(150)]], + password: ["", [Validators.required]], + }); + + submit = async () => { + await this.dialogService.openSimpleDialog({ + title: "Confirm", + content: "Are you sure you want to submit?", + type: "primary", + acceptButtonText: "Yes", + cancelButtonText: "No", + acceptAction: async () => this.acceptDialog(), + }); + }; + + acceptDialog() { + this.formObj.markAllAsTouched(); + this.dialogService.closeAll(); + } + + colors = [ + { value: "blue", name: "Blue" }, + { value: "white", name: "White" }, + { value: "gray", name: "Gray" }, + ]; + + worstPasswords = [ + { id: "1", listName: "1234", labelName: "1234" }, + { id: "2", listName: "admin", labelName: "admin" }, + { id: "3", listName: "password", labelName: "password" }, + { id: "4", listName: "querty", labelName: "querty" }, + { id: "5", listName: "letmein", labelName: "letmein" }, + { id: "6", listName: "trustno1", labelName: "trustno1" }, + { id: "7", listName: "1qaz2wsx", labelName: "1qaz2wsx" }, + ]; +} diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts new file mode 100644 index 0000000000..ae4448eb76 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-main.component.ts @@ -0,0 +1,117 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { Component } from "@angular/core"; + +import { DialogService } from "../../../dialog"; +import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; + +import { KitchenSinkForm } from "./kitchen-sink-form.component"; +import { KitchenSinkTable } from "./kitchen-sink-table.component"; +import { KitchenSinkToggleList } from "./kitchen-sink-toggle-list.component"; + +@Component({ + standalone: true, + imports: [KitchenSinkSharedModule], + template: ` + <bit-dialog title="Dialog Title" dialogSize="large"> + <span bitDialogContent> Dialog body text goes here. </span> + <ng-container bitDialogFooter> + <button bitButton buttonType="primary" (click)="dialogRef.close()">OK</button> + <button bitButton buttonType="secondary" bitDialogClose>Cancel</button> + </ng-container> + </bit-dialog> + `, +}) +class KitchenSinkDialog { + constructor(public dialogRef: DialogRef) {} +} + +@Component({ + standalone: true, + selector: "bit-tab-main", + imports: [ + KitchenSinkSharedModule, + KitchenSinkTable, + KitchenSinkToggleList, + KitchenSinkForm, + KitchenSinkDialog, + ], + template: ` + <bit-banner bannerType="info" class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"> + Kitchen Sink test zone + </bit-banner> + + <p class="tw-mt-4"> + <bit-breadcrumbs> + <bit-breadcrumb *ngFor="let item of navItems" [icon]="item.icon" [route]="[item.route]"> + {{ item.name }} + </bit-breadcrumb> + </bit-breadcrumbs> + </p> + + <bit-callout type="info" title="About the Kitchen Sink"> + <p bitTypography="body1"> + The purpose of this story is to compose together all of our components. When snapshot tests + run, we'll be able to spot-check visual changes in a more app-like environment than just the + isolated stories. The stories for the Kitchen Sink exist to be tested by the Chromatic UI + tests. + </p> + + <p bitTypography="body1"> + NOTE: These stories will treat "Light & Dark" mode as "Light" mode. This is done to avoid a + bug with the way that we render the same component twice in the same iframe and how that + interacts with the <code>router-outlet</code>. + </p> + </bit-callout> + + <div class="tw-mb-6 tw-mt-6"> + <h1 bitTypography="h1" class="tw-text-main"> + Bitwarden <bit-avatar text="Bit Warden"></bit-avatar> + </h1> + <a bitLink linkType="primary" href="#">Learn more</a> + </div> + + <bit-tab-group label="Main content tabs" class="tw-text-main"> + <bit-tab label="Evaluation"> + <bit-section> + <h2 bitTypography="h2" class="tw-text-main tw-mb-6">About</h2> + <bit-kitchen-sink-table></bit-kitchen-sink-table> + + <button bitButton (click)="openDefaultDialog()">Open Dialog</button> + </bit-section> + <bit-section> + <h2 bitTypography="h2" class="tw-text-main tw-mb-6">Companies using Bitwarden</h2> + <bit-kitchen-sink-toggle-list></bit-kitchen-sink-toggle-list> + </bit-section> + <bit-section> + <h2 bitTypography="h2" class="tw-text-main tw-mb-6">Survey</h2> + <bit-kitchen-sink-form></bit-kitchen-sink-form> + </bit-section> + </bit-tab> + + <bit-tab label="Empty tab" data-testid="empty-tab"> + <bit-section> + <bit-no-items class="tw-text-main"> + <ng-container slot="title">This tab is empty</ng-container> + <ng-container slot="description"> + <p bitTypography="body2">Try searching for what you are looking for:</p> + <bit-search></bit-search> + <p bitTypography="helper">Note that the search bar is not functional</p> + </ng-container> + </bit-no-items> + </bit-section> + </bit-tab> + </bit-tab-group> + `, +}) +export class KitchenSinkMainComponent { + constructor(public dialogService: DialogService) {} + + openDefaultDialog() { + this.dialogService.open(KitchenSinkDialog); + } + + navItems = [ + { icon: "bwi-collection", name: "Password Managers", route: "/" }, + { icon: "bwi-collection", name: "Favorites", route: "/" }, + ]; +} diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts new file mode 100644 index 0000000000..3c6d6f1144 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-table.component.ts @@ -0,0 +1,49 @@ +import { Component } from "@angular/core"; + +import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; + +@Component({ + standalone: true, + selector: "bit-kitchen-sink-table", + imports: [KitchenSinkSharedModule], + template: ` + <bit-table> + <ng-container header> + <tr> + <th bitCell>Product</th> + <th bitCell>User</th> + <th bitCell>Options</th> + </tr> + </ng-container> + <ng-template body> + <tr bitRow> + <td bitCell>Password Manager</td> + <td bitCell>Everyone</td> + <td bitCell> + <button bitIconButton="bwi-ellipsis-v" [bitMenuTriggerFor]="menu1"></button> + <bit-menu #menu1> + <a href="#" bitMenuItem>Anchor link</a> + <a href="#" bitMenuItem>Another link</a> + <bit-menu-divider></bit-menu-divider> + <button type="button" bitMenuItem>Button after divider</button> + </bit-menu> + </td> + </tr> + <tr bitRow> + <td bitCell>Secrets Manager</td> + <td bitCell>Developers</td> + <td bitCell> + <button bitIconButton="bwi-ellipsis-v" [bitMenuTriggerFor]="menu2"></button> + <bit-menu #menu2> + <a href="#" bitMenuItem>Anchor link</a> + <a href="#" bitMenuItem>Another link</a> + <bit-menu-divider></bit-menu-divider> + <button type="button" bitMenuItem>Button after divider</button> + </bit-menu> + </td> + </tr> + </ng-template> + </bit-table> + `, +}) +export class KitchenSinkTable {} diff --git a/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts new file mode 100644 index 0000000000..2804c9e835 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/components/kitchen-sink-toggle-list.component.ts @@ -0,0 +1,34 @@ +import { Component } from "@angular/core"; + +import { KitchenSinkSharedModule } from "../kitchen-sink-shared.module"; + +@Component({ + standalone: true, + selector: "bit-kitchen-sink-toggle-list", + imports: [KitchenSinkSharedModule], + template: ` + <div class="tw-mt-6 tw-mb-6"> + <bit-toggle-group [(selected)]="selectedToggle" aria-label="Company list filter"> + <bit-toggle value="all"> All <span bitBadge variant="info">3</span> </bit-toggle> + + <bit-toggle value="large"> Enterprise <span bitBadge variant="info">2</span> </bit-toggle> + + <bit-toggle value="small"> Mid-sized <span bitBadge variant="info">1</span> </bit-toggle> + </bit-toggle-group> + </div> + <ul *ngFor="let company of companyList"> + <li *ngIf="company.size === selectedToggle || selectedToggle === 'all'"> + {{ company.name }} + </li> + </ul> + `, +}) +export class KitchenSinkToggleList { + selectedToggle: "all" | "large" | "small" = "all"; + + companyList = [ + { name: "A large enterprise company", size: "large" }, + { name: "Another enterprise company", size: "large" }, + { name: "A smaller company", size: "small" }, + ]; +} diff --git a/libs/components/src/stories/kitchen-sink/index.ts b/libs/components/src/stories/kitchen-sink/index.ts new file mode 100644 index 0000000000..1d13b23136 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/index.ts @@ -0,0 +1 @@ +export * from "./kitchen-sink.stories"; diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts new file mode 100644 index 0000000000..56e3a92e2a --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink-shared.module.ts @@ -0,0 +1,114 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { RouterModule } from "@angular/router"; + +import { AsyncActionsModule } from "../../async-actions"; +import { AvatarModule } from "../../avatar"; +import { BadgeModule } from "../../badge"; +import { BannerModule } from "../../banner"; +import { BreadcrumbsModule } from "../../breadcrumbs"; +import { ButtonModule } from "../../button"; +import { CalloutModule } from "../../callout"; +import { CheckboxModule } from "../../checkbox"; +import { ColorPasswordModule } from "../../color-password"; +import { DialogModule } from "../../dialog"; +import { FormControlModule } from "../../form-control"; +import { FormFieldModule } from "../../form-field"; +import { IconModule } from "../../icon"; +import { IconButtonModule } from "../../icon-button"; +import { InputModule } from "../../input"; +import { LayoutComponent } from "../../layout"; +import { LinkModule } from "../../link"; +import { MenuModule } from "../../menu"; +import { NavigationModule } from "../../navigation"; +import { NoItemsModule } from "../../no-items"; +import { PopoverModule } from "../../popover"; +import { ProgressModule } from "../../progress"; +import { RadioButtonModule } from "../../radio-button"; +import { SearchModule } from "../../search"; +import { SectionComponent } from "../../section"; +import { SelectModule } from "../../select"; +import { SharedModule } from "../../shared"; +import { TableModule } from "../../table"; +import { TabsModule } from "../../tabs"; +import { ToggleGroupModule } from "../../toggle-group"; +import { TypographyModule } from "../../typography"; + +@NgModule({ + imports: [ + AsyncActionsModule, + AvatarModule, + BadgeModule, + BannerModule, + BreadcrumbsModule, + ButtonModule, + CalloutModule, + CheckboxModule, + ColorPasswordModule, + CommonModule, + DialogModule, + FormControlModule, + FormFieldModule, + FormsModule, + IconButtonModule, + IconModule, + InputModule, + LayoutComponent, + LinkModule, + MenuModule, + NavigationModule, + NoItemsModule, + PopoverModule, + ProgressModule, + RadioButtonModule, + ReactiveFormsModule, + RouterModule, + SearchModule, + SectionComponent, + SelectModule, + SharedModule, + TableModule, + TabsModule, + ToggleGroupModule, + TypographyModule, + ], + exports: [ + AsyncActionsModule, + AvatarModule, + BadgeModule, + BannerModule, + BreadcrumbsModule, + ButtonModule, + CalloutModule, + CheckboxModule, + ColorPasswordModule, + CommonModule, + DialogModule, + FormControlModule, + FormFieldModule, + FormsModule, + IconButtonModule, + IconModule, + InputModule, + LayoutComponent, + LinkModule, + MenuModule, + NavigationModule, + NoItemsModule, + PopoverModule, + ProgressModule, + RadioButtonModule, + ReactiveFormsModule, + RouterModule, + SearchModule, + SectionComponent, + SelectModule, + SharedModule, + TableModule, + TabsModule, + ToggleGroupModule, + TypographyModule, + ], +}) +export class KitchenSinkSharedModule {} diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.mdx b/libs/components/src/stories/kitchen-sink/kitchen-sink.mdx new file mode 100644 index 0000000000..49493f749e --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.mdx @@ -0,0 +1,15 @@ +import { Meta, Story } from "@storybook/addon-docs"; + +import * as stories from "./kitchen-sink.stories"; + +<Meta of={stories} /> + +# Kitchen Sink + +The purpose of this story is to compose together all of our components. When snapshot tests run, +we'll be able to spot-check visual changes in a more app-like environment than just the isolated +stories. The stories for the Kitchen Sink exist to be tested by the Chromatic UI tests. + +NOTE: These stories will treat "Light & Dark" mode as "Light" mode. This is done to avoid a bug with +the way that we render the same component twice in the same iframe and how that interacts with the +`router-outlet`. diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts new file mode 100644 index 0000000000..70adb21191 --- /dev/null +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -0,0 +1,172 @@ +import { importProvidersFrom } from "@angular/core"; +import { provideNoopAnimations } from "@angular/platform-browser/animations"; +import { RouterModule } from "@angular/router"; +import { + Meta, + StoryObj, + applicationConfig, + componentWrapperDecorator, + moduleMetadata, +} from "@storybook/angular"; +import { + userEvent, + getAllByRole, + getByRole, + getByLabelText, + fireEvent, +} from "@storybook/testing-library"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { DialogService } from "../../dialog"; +import { LayoutComponent } from "../../layout"; +import { I18nMockService } from "../../utils/i18n-mock.service"; + +import { KitchenSinkForm } from "./components/kitchen-sink-form.component"; +import { KitchenSinkMainComponent } from "./components/kitchen-sink-main.component"; +import { KitchenSinkTable } from "./components/kitchen-sink-table.component"; +import { KitchenSinkToggleList } from "./components/kitchen-sink-toggle-list.component"; +import { KitchenSinkSharedModule } from "./kitchen-sink-shared.module"; + +export default { + title: "Documentation / Kitchen Sink", + component: LayoutComponent, + decorators: [ + componentWrapperDecorator( + /** + * Applying a CSS transform makes a `position: fixed` element act like it is `position: relative` + * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 + */ + (story) => { + return /* HTML */ `<div class="tw-scale-100 tw-border-2 tw-border-solid tw-border-[red]"> + ${story} + </div>`; + }, + ({ globals }) => { + /** + * avoid a bug with the way that we render the same component twice in the same iframe and how + * that interacts with the router-outlet + */ + const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; + return { theme: themeOverride }; + }, + ), + moduleMetadata({ + imports: [ + KitchenSinkSharedModule, + KitchenSinkForm, + KitchenSinkMainComponent, + KitchenSinkTable, + KitchenSinkToggleList, + ], + providers: [ + DialogService, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + search: "Search", + skipToContent: "Skip to content", + submenu: "submenu", + toggleCollapse: "toggle collapse", + }); + }, + }, + ], + }), + applicationConfig({ + providers: [ + provideNoopAnimations(), + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "bitwarden", pathMatch: "full" }, + { path: "bitwarden", component: KitchenSinkMainComponent }, + ], + { useHash: true }, + ), + ), + ], + }), + ], +} as Meta; + +type Story = StoryObj<LayoutComponent>; + +export const Default: Story = { + render: (args) => { + return { + props: args, + template: /* HTML */ `<bit-layout> + <nav slot="sidebar"> + <bit-nav-group text="Password Managers" icon="bwi-collection" [open]="true"> + <bit-nav-group text="Favorites" icon="bwi-collection" variant="tree" [open]="true"> + <bit-nav-item text="Bitwarden" route="bitwarden"></bit-nav-item> + <bit-nav-divider></bit-nav-divider> + </bit-nav-group> + </bit-nav-group> + </nav> + <router-outlet></router-outlet> + </bit-layout>`, + }; + }, +}; + +export const MenuOpen: Story = { + ...Default, + play: async (context) => { + const canvas = context.canvasElement; + const table = getByRole(canvas, "table"); + + const menuButton = getAllByRole(table, "button")[0]; + await userEvent.click(menuButton); + }, +}; + +export const DefaultDialogOpen: Story = { + ...Default, + play: (context) => { + const canvas = context.canvasElement; + const dialogButton = getByRole(canvas, "button", { + name: "Open Dialog", + }); + + // workaround for userEvent not firing in FF https://github.com/testing-library/user-event/issues/1075 + fireEvent.click(dialogButton); + }, +}; + +export const PopoverOpen: Story = { + ...Default, + play: async (context) => { + const canvas = context.canvasElement; + const passwordLabelIcon = getByLabelText(canvas, "A random password (required)", { + selector: "button", + }); + + await userEvent.click(passwordLabelIcon); + }, +}; + +export const SimpleDialogOpen: Story = { + ...Default, + play: (context) => { + const canvas = context.canvasElement; + const submitButton = getByRole(canvas, "button", { + name: "Submit", + }); + + // workaround for userEvent not firing in FF https://github.com/testing-library/user-event/issues/1075 + fireEvent.click(submitButton); + }, +}; + +export const EmptyTab: Story = { + ...Default, + play: async (context) => { + const canvas = context.canvasElement; + const emptyTab = getByRole(canvas, "tab", { name: "Empty tab" }); + await userEvent.click(emptyTab); + }, +}; diff --git a/libs/components/src/stories/migration.mdx b/libs/components/src/stories/migration.mdx index a18ba6ad22..83d6d0666d 100644 --- a/libs/components/src/stories/migration.mdx +++ b/libs/components/src/stories/migration.mdx @@ -49,14 +49,14 @@ Only use Tailwind for styling. No Bootstrap or other custom CSS is allowed. This is easy to verify. Bitwarden prefixes all Tailwind classes with `tw-`. If you see a class without this prefix, it probably shouldn't be there. -<div class="tw-bg-danger-500/10 tw-p-4"> +<div class="tw-bg-danger-600/10 tw-p-4"> <span class="tw-font-bold tw-text-danger">Bad (Bootstrap)</span> ```html <div class="mb-2"></div> ``` </div> -<div class="tw-bg-success-500/10 tw-p-4"> +<div class="tw-bg-success-600/10 tw-p-4"> <span class="tw-font-bold tw-text-success">Good (Tailwind)</span> ```html <div class="tw-mb-2"></div> @@ -65,7 +65,7 @@ without this prefix, it probably shouldn't be there. **Exception:** Icon font classes, prefixed with `bwi`, are allowed. -<div class="tw-bg-success-500/10 tw-p-4"> +<div class="tw-bg-success-600/10 tw-p-4"> <span class="tw-font-bold tw-text-success">Good (Icons)</span> ```html <i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i> @@ -79,7 +79,7 @@ The CL has form components that integrate with Angular's reactive forms: `bit-fo reactive forms to make use of these components. Review the [form component docs](?path=/docs/component-library-form--docs). -<div class="tw-bg-danger-500/10 tw-p-4"> +<div class="tw-bg-danger-600/10 tw-p-4"> <span class="tw-text-danger tw-font-bold">Bad</span> ```html <form #form (ngSubmit)="submit()"> @@ -88,7 +88,7 @@ reactive forms to make use of these components. Review the ``` </div> -<div class="tw-bg-success-500/10 tw-p-4"> +<div class="tw-bg-success-600/10 tw-p-4"> <span class="tw-text-success tw-font-bold">Good</span> ```html <form [formGroup]="formGroup" [bitSubmit]="submit"> @@ -105,14 +105,14 @@ fully migrated should have no reference to the `ModalService`. 1. Update the template to use CL components: -<div class="tw-bg-danger-500/10 tw-p-4"> +<div class="tw-bg-danger-600/10 tw-p-4"> ```html <!-- FooDialogComponent --> <div class="modal fade" role="dialog" aria-modal="true">...</div> ``` </div> -<div class="tw-bg-success-500/10 tw-p-4"> +<div class="tw-bg-success-600/10 tw-p-4"> ```html <!-- FooDialogComponent --> <bit-dialog>...</bit-dialog> @@ -121,7 +121,7 @@ fully migrated should have no reference to the `ModalService`. 2. Create a static `open` method on the component, that calls `DialogService.open`: -<div class="tw-bg-success-500/10 tw-p-4"> +<div class="tw-bg-success-600/10 tw-p-4"> ```ts export class FooDialogComponent { //... @@ -137,7 +137,7 @@ fully migrated should have no reference to the `ModalService`. 3. If you need to pass data into the dialog, pass it to `open` as a parameter and inject `DIALOG_DATA` into the component's constructor. -<div class="tw-bg-success-500/10 tw-p-4"> +<div class="tw-bg-success-600/10 tw-p-4"> ```ts export type FooDialogParams = { bar: string; @@ -157,9 +157,9 @@ fully migrated should have no reference to the `ModalService`. 4. Replace calls to `ModalService.open` or `ModalService.openViewRef` with the newly created static `open` method: -<div class="tw-bg-danger-500/10 tw-p-4">`this.modalService.open(FooDialogComponent);`</div> +<div class="tw-bg-danger-600/10 tw-p-4">`this.modalService.open(FooDialogComponent);`</div> -<div class="tw-bg-success-500/10 tw-p-4">`FooDialogComponent.open(this.dialogService);`</div> +<div class="tw-bg-success-600/10 tw-p-4">`FooDialogComponent.open(this.dialogService);`</div> ## Examples diff --git a/libs/components/src/tabs/shared/tab-list-item.directive.ts b/libs/components/src/tabs/shared/tab-list-item.directive.ts index 74a87b1d29..4b2030388f 100644 --- a/libs/components/src/tabs/shared/tab-list-item.directive.ts +++ b/libs/components/src/tabs/shared/tab-list-item.directive.ts @@ -42,7 +42,7 @@ export class TabListItemDirective implements FocusableOption { return ["!tw-text-muted", "hover:!tw-text-muted"]; } if (this.active) { - return ["!tw-text-primary-500", "hover:!tw-text-primary-700"]; + return ["!tw-text-primary-600", "hover:!tw-text-primary-700"]; } return ["!tw-text-main", "hover:!tw-text-main"]; } @@ -78,7 +78,7 @@ export class TabListItemDirective implements FocusableOption { return [ "tw--mb-px", "tw-border-x-secondary-300", - "tw-border-t-primary-500", + "tw-border-t-primary-600", "tw-border-b", "tw-border-b-background", "!tw-bg-background", diff --git a/libs/components/src/toggle-group/toggle.component.ts b/libs/components/src/toggle-group/toggle.component.ts index 55c678d017..7d227acde3 100644 --- a/libs/components/src/toggle-group/toggle.component.ts +++ b/libs/components/src/toggle-group/toggle.component.ts @@ -50,10 +50,10 @@ export class ToggleComponent<TValue> { "peer-focus:tw-outline-none", "peer-focus:tw-ring", "peer-focus:tw-ring-offset-2", - "peer-focus:tw-ring-primary-500", + "peer-focus:tw-ring-primary-600", "peer-focus:tw-z-10", - "peer-focus:tw-bg-primary-500", - "peer-focus:tw-border-primary-500", + "peer-focus:tw-bg-primary-600", + "peer-focus:tw-border-primary-600", "peer-focus:!tw-text-contrast", "hover:tw-no-underline", @@ -61,8 +61,8 @@ export class ToggleComponent<TValue> { "hover:tw-border-text-muted", "hover:!tw-text-contrast", - "peer-checked:tw-bg-primary-500", - "peer-checked:tw-border-primary-500", + "peer-checked:tw-bg-primary-600", + "peer-checked:tw-border-primary-600", "peer-checked:!tw-text-contrast", "tw-py-1.5", "tw-px-3", diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 75a8fa6380..0087af28ae 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -1,5 +1,13 @@ @import "./reset.css"; +/** + Note that the value of the *-600 colors is currently equivalent to the value + of the *-500 variant of that color. This is a temporary change to make BW-42 + updates easier. + + TODO remove comment when the color palette portion of BW-42 is completed. +*/ + :root { --color-transparent-hover: rgb(0 0 0 / 0.03); @@ -10,24 +18,24 @@ --color-background-alt4: 13 60 119; --color-primary-300: 103 149 232; - --color-primary-500: 23 93 220; + --color-primary-600: 23 93 220; --color-primary-700: 18 82 163; --color-secondary-100: 240 240 240; --color-secondary-300: 206 212 220; - --color-secondary-500: 137 146 159; + --color-secondary-600: 137 146 159; --color-secondary-700: 33 37 41; - --color-success-500: 1 126 69; + --color-success-600: 1 126 69; --color-success-700: 0 85 46; - --color-danger-500: 200 53 34; + --color-danger-600: 200 53 34; --color-danger-700: 152 41 27; - --color-warning-500: 139 102 9; + --color-warning-600: 139 102 9; --color-warning-700: 105 77 5; - --color-info-500: 85 85 85; + --color-info-600: 85 85 85; --color-info-700: 59 58 58; --color-text-main: 33 37 41; @@ -53,24 +61,24 @@ --color-background-alt4: 16 18 21; --color-primary-300: 23 93 220; - --color-primary-500: 106 153 240; + --color-primary-600: 106 153 240; --color-primary-700: 180 204 249; --color-secondary-100: 47 52 61; --color-secondary-300: 110 118 137; - --color-secondary-500: 186 192 206; + --color-secondary-600: 186 192 206; --color-secondary-700: 255 255 255; - --color-success-500: 82 224 124; + --color-success-600: 82 224 124; --color-success-700: 168 239 190; - --color-danger-500: 255 141 133; + --color-danger-600: 255 141 133; --color-danger-700: 255 191 187; - --color-warning-500: 255 235 102; + --color-warning-600: 255 235 102; --color-warning-700: 255 245 179; - --color-info-500: 164 176 198; + --color-info-600: 164 176 198; --color-info-700: 209 215 226; --color-text-main: 255 255 255; @@ -92,24 +100,24 @@ --color-background-alt4: 67 76 94; --color-primary-300: 108 153 166; - --color-primary-500: 136 192 208; + --color-primary-600: 136 192 208; --color-primary-700: 160 224 242; --color-secondary-100: 76 86 106; --color-secondary-300: 94 105 125; - --color-secondary-500: 216 222 233; + --color-secondary-600: 216 222 233; --color-secondary-700: 255 255 255; - --color-success-500: 163 190 140; + --color-success-600: 163 190 140; --color-success-700: 144 170 122; - --color-danger-500: 228 129 139; + --color-danger-600: 228 129 139; --color-danger-700: 191 97 106; - --color-warning-500: 235 203 139; + --color-warning-600: 235 203 139; --color-warning-700: 210 181 121; - --color-info-500: 129 161 193; + --color-info-600: 129 161 193; --color-info-700: 94 129 172; --color-text-main: 229 233 240; @@ -131,24 +139,24 @@ --color-background-alt4: 0 43 54; --color-primary-300: 42 161 152; - --color-primary-500: 133 153 0; + --color-primary-600: 133 153 0; --color-primary-700: 192 203 123; --color-secondary-100: 31 72 87; --color-secondary-300: 101 123 131; - --color-secondary-500: 131 148 150; + --color-secondary-600: 131 148 150; --color-secondary-700: 238 232 213; - --color-success-500: 133 153 0; + --color-success-600: 133 153 0; --color-success-700: 192 203 123; - --color-danger-500: 220 50 47; + --color-danger-600: 220 50 47; --color-danger-700: 223 135 134; - --color-warning-500: 181 137 0; + --color-warning-600: 181 137 0; --color-warning-700: 220 189 92; - --color-info-500: 133 153 0; + --color-info-600: 133 153 0; --color-info-700: 192 203 123; --color-text-main: 253 246 227; diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 5f49c6fc26..b76f25eae7 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -25,29 +25,29 @@ module.exports = { black: colors.black, primary: { 300: rgba("--color-primary-300"), - 500: rgba("--color-primary-500"), + 600: rgba("--color-primary-600"), 700: rgba("--color-primary-700"), }, secondary: { 100: rgba("--color-secondary-100"), 300: rgba("--color-secondary-300"), - 500: rgba("--color-secondary-500"), + 600: rgba("--color-secondary-600"), 700: rgba("--color-secondary-700"), }, success: { - 500: rgba("--color-success-500"), + 600: rgba("--color-success-600"), 700: rgba("--color-success-700"), }, danger: { - 500: rgba("--color-danger-500"), + 600: rgba("--color-danger-600"), 700: rgba("--color-danger-700"), }, warning: { - 500: rgba("--color-warning-500"), + 600: rgba("--color-warning-600"), 700: rgba("--color-warning-700"), }, info: { - 500: rgba("--color-info-500"), + 600: rgba("--color-info-600"), 700: rgba("--color-info-700"), }, text: { @@ -71,13 +71,13 @@ module.exports = { contrast: rgba("--color-text-contrast"), alt2: rgba("--color-text-alt2"), code: rgba("--color-text-code"), - success: rgba("--color-success-500"), - danger: rgba("--color-danger-500"), - warning: rgba("--color-warning-500"), - info: rgba("--color-info-500"), + success: rgba("--color-success-600"), + danger: rgba("--color-danger-600"), + warning: rgba("--color-warning-600"), + info: rgba("--color-info-600"), primary: { 300: rgba("--color-primary-300"), - 500: rgba("--color-primary-500"), + 600: rgba("--color-primary-600"), 700: rgba("--color-primary-700"), }, }, diff --git a/libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts b/libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts new file mode 100644 index 0000000000..ef92ea7984 --- /dev/null +++ b/libs/importer/spec/test-data/bitwarden-json/cipher-with-collections.json.ts @@ -0,0 +1,37 @@ +export const cipherWithCollections = `{ + "encrypted": false, + "collections": [ + { + "id": "8e3f5ba1-3e87-4ee8-8da9-b1180099ff9f", + "organizationId": "c6181652-66eb-4cd9-a7f2-b02a00e12352", + "name": "asdf", + "externalId": null + } + ], + "items": [ + { + "passwordHistory": null, + "revisionDate": "2024-02-16T09:20:48.383Z", + "creationDate": "2024-02-16T09:20:48.383Z", + "deletedDate": null, + "id": "f761a968-4b0f-4090-a568-b118009a07b5", + "organizationId": "c6181652-66eb-4cd9-a7f2-b02a00e12352", + "folderId": null, + "type": 1, + "reprompt": 0, + "name": "asdf123", + "notes": null, + "favorite": false, + "login": { + "fido2Credentials": [], + "uris": [], + "username": null, + "password": null, + "totp": null + }, + "collectionIds": [ + "8e3f5ba1-3e87-4ee8-8da9-b1180099ff9f" + ] + } + ] + }`; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index a95b74d792..7bbcd3287a 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -6,6 +6,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -195,7 +196,7 @@ describe("ImportService", () => { new Object() as FolderView, ); - await expect(setImportTargetMethod).rejects.toThrow("Error assigning target collection"); + await expect(setImportTargetMethod).rejects.toThrow(); }); it("passing importTarget as null on setImportTarget throws error", async () => { @@ -205,7 +206,62 @@ describe("ImportService", () => { new Object() as CollectionView, ); - await expect(setImportTargetMethod).rejects.toThrow("Error assigning target folder"); + await expect(setImportTargetMethod).rejects.toThrow(); + }); + + it("passing importTarget, collectionRelationship has the expected values", async () => { + collectionService.getAllDecrypted.mockResolvedValue([ + mockImportTargetCollection, + mockCollection1, + mockCollection2, + ]); + + importResult.ciphers.push(createCipher({ name: "cipher1" })); + importResult.ciphers.push(createCipher({ name: "cipher2" })); + importResult.collectionRelationships.push([0, 0]); + importResult.collections.push(mockCollection1); + importResult.collections.push(mockCollection2); + + await importService["setImportTarget"]( + importResult, + organizationId, + mockImportTargetCollection, + ); + expect(importResult.collectionRelationships.length).toEqual(2); + expect(importResult.collectionRelationships[0]).toEqual([1, 0]); + expect(importResult.collectionRelationships[1]).toEqual([0, 1]); + }); + + it("passing importTarget, folderRelationship has the expected values", async () => { + folderService.getAllDecryptedFromState.mockResolvedValue([ + mockImportTargetFolder, + mockFolder1, + mockFolder2, + ]); + + importResult.folders.push(mockFolder1); + importResult.folders.push(mockFolder2); + + importResult.ciphers.push(createCipher({ name: "cipher1", folderId: mockFolder1.id })); + importResult.ciphers.push(createCipher({ name: "cipher2" })); + importResult.folderRelationships.push([0, 0]); + + await importService["setImportTarget"](importResult, "", mockImportTargetFolder); + expect(importResult.folderRelationships.length).toEqual(2); + expect(importResult.folderRelationships[0]).toEqual([1, 0]); + expect(importResult.folderRelationships[1]).toEqual([0, 1]); }); }); }); + +function createCipher(options: Partial<CipherView> = {}) { + const cipher = new CipherView(); + + cipher.name; + cipher.type = options.type; + cipher.folderId = options.folderId; + cipher.collectionIds = options.collectionIds; + cipher.organizationId = options.organizationId; + + return cipher; +} diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index a6fd233dcf..f5cab933f3 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -432,13 +432,15 @@ export class ImportService implements ImportServiceAbstraction { if (organizationId) { if (!(importTarget instanceof CollectionView)) { - throw new Error("Error assigning target collection"); + throw new Error(this.i18nService.t("errorAssigningTargetCollection")); } const noCollectionRelationShips: [number, number][] = []; importResult.ciphers.forEach((c, index) => { - if (!Array.isArray(c.collectionIds) || c.collectionIds.length == 0) { - c.collectionIds = [importTarget.id]; + if ( + !Array.isArray(importResult.collectionRelationships) || + !importResult.collectionRelationships.some(([cipherPos]) => cipherPos === index) + ) { noCollectionRelationShips.push([index, 0]); } }); @@ -461,7 +463,7 @@ export class ImportService implements ImportServiceAbstraction { } if (!(importTarget instanceof FolderView)) { - throw new Error("Error assigning target folder"); + throw new Error(this.i18nService.t("errorAssigningTargetFolder")); } const noFolderRelationShips: [number, number][] = []; diff --git a/libs/shared/jest.config.angular.js b/libs/shared/jest.config.angular.js index a0dcc27516..689a04d858 100644 --- a/libs/shared/jest.config.angular.js +++ b/libs/shared/jest.config.angular.js @@ -6,6 +6,11 @@ const { defaultTransformerOptions } = require("jest-preset-angular/presets"); module.exports = { testMatch: ["**/+(*.)+(spec).+(ts)"], + testPathIgnorePatterns: [ + "/node_modules/", // default value + ".*.type.spec.ts", // ignore type tests (which are checked at compile time and not run by jest) + ], + // 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 diff --git a/libs/angular/src/tools/export/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts similarity index 98% rename from libs/angular/src/tools/export/components/export.component.ts rename to libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 400071e59c..ce478db19a 100644 --- a/libs/angular/src/tools/export/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -2,6 +2,7 @@ import { Directive, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@ import { UntypedFormBuilder, Validators } from "@angular/forms"; import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs"; +import { PasswordStrengthComponent } from "@bitwarden/angular/tools/password-strength/password-strength.component"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -18,8 +19,6 @@ import { EncryptedExportType } from "@bitwarden/common/tools/enums/encrypted-exp import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; -import { PasswordStrengthComponent } from "../../password-strength/password-strength.component"; - @Directive() export class ExportComponent implements OnInit, OnDestroy { @Output() onSaved = new EventEmitter(); diff --git a/libs/tools/export/vault-export/vault-export-ui/src/index.ts b/libs/tools/export/vault-export/vault-export-ui/src/index.ts index 4165ee4558..919bc8b38e 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/index.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/index.ts @@ -1 +1,2 @@ +export { ExportComponent } from "./components/export.component"; export { ExportScopeCalloutComponent } from "./components/export-scope-callout.component"; diff --git a/package-lock.json b/package-lock.json index b899863568..d72ba9cb19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "bufferutil": "4.0.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.34.0", + "core-js": "3.36.1", "duo_web_sdk": "github:duosecurity/duo_web_sdk", "form-data": "4.0.0", "https-proxy-agent": "7.0.2", @@ -67,7 +67,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.13", + "tldts": "6.1.16", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -108,7 +108,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "18.19.19", + "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.0", @@ -117,17 +117,17 @@ "@types/react": "16.14.57", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "6.21.0", - "@typescript-eslint/parser": "6.21.0", + "@typescript-eslint/eslint-plugin": "7.4.0", + "@typescript-eslint/parser": "7.4.0", "@webcomponents/custom-elements": "1.6.0", "autoprefixer": "10.4.18", "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", - "copy-webpack-plugin": "11.0.0", + "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", - "css-loader": "6.8.1", - "electron": "28.2.8", + "css-loader": "6.10.0", + "electron": "28.3.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", @@ -147,28 +147,28 @@ "gulp-json-editor": "2.6.0", "gulp-replace": "1.1.4", "gulp-zip": "6.0.0", - "html-loader": "4.2.0", + "html-loader": "5.0.0", "html-webpack-injector": "1.1.4", - "html-webpack-plugin": "5.5.4", + "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.5", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", - "mini-css-extract-plugin": "2.7.6", + "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", "pkg": "5.8.1", - "postcss": "8.4.35", - "postcss-loader": "7.3.4", + "postcss": "8.4.38", + "postcss-loader": "8.1.1", "prettier": "3.2.2", - "prettier-plugin-tailwindcss": "0.5.12", + "prettier-plugin-tailwindcss": "0.5.13", "process": "0.11.10", "react": "18.2.0", "react-dom": "18.2.0", "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", - "sass": "1.69.5", + "sass": "1.74.1", "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4", @@ -183,21 +183,21 @@ "wait-on": "7.2.0", "webpack": "5.89.0", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1", + "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0" }, "engines": { - "node": "~18", + "node": "^18.18.0", "npm": "~9" } }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.3.0" + "version": "2024.4.1" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.3.0", + "version": "2024.3.1", "license": "GPL-3.0-only", "dependencies": { "@koa/multer": "3.0.2", @@ -224,7 +224,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.13", + "tldts": "6.1.16", "zxcvbn": "4.4.2" }, "bin": { @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.3.1", + "version": "2024.4.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -245,25 +245,9 @@ "@napi-rs/cli": "2.16.2" } }, - "apps/desktop/node_modules/@napi-rs/cli": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.2.tgz", - "integrity": "sha512-U2aZfnr0s9KkXpZlYC0l5WxWCXL7vJUNpCnWMwq3T9GG9rhYAAUM9CTZsi1Z+0iR2LcHbfq9EfMgoqnuTyUjfg==", - "dev": true, - "bin": { - "napi": "scripts/index.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.3.0" + "version": "2024.4.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -339,9 +323,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz", - "integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", "dev": true }, "node_modules/@aduh95/viz.js": { @@ -375,16 +359,17 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1602.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.11.tgz", - "integrity": "sha512-qC1tPL/82gxqCS1z9pTpLn5NQH6uqbV6UNjbkFEQpTwEyWEK6VLChAJsybHHfbpssPS2HWf31VoUzX7RqDjoQQ==", + "version": "0.1703.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.2.tgz", + "integrity": "sha512-fT5gSzwDHOyGv8zF97t8rjeoYSGSxXjWWstl3rN1nXdO0qgJ5m6Sv0fupON+HltdXDCBLRH+2khNpqx/Fh0Qww==", "dev": true, + "peer": true, "dependencies": { - "@angular-devkit/core": "16.2.11", + "@angular-devkit/core": "17.3.2", "rxjs": "7.8.1" }, "engines": { - "node": "^16.14.0 || >=18.10.0", + "node": "^18.13.0 || >=20.9.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } @@ -511,6 +496,67 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.1602.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.11.tgz", + "integrity": "sha512-qC1tPL/82gxqCS1z9pTpLn5NQH6uqbV6UNjbkFEQpTwEyWEK6VLChAJsybHHfbpssPS2HWf31VoUzX7RqDjoQQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.2.11", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/build-webpack": { + "version": "0.1602.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.11.tgz", + "integrity": "sha512-2Au6xRMxNugFkXP0LS1TwNE5gAfGW4g6yxC9P5j5p3kdGDnAVaZRTOKB9dg73i3uXtJHUMciYOThV0b78XRxwA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1602.11", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.11.tgz", + "integrity": "sha512-u3cEQHqhSMWyAFIaPdRukCJwEUJt7Fy3C02gTlTeCB4F/OnftVFIm2e5vmCqMo9rgbfdvjWj9V+7wWiCpMrzAQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", @@ -550,35 +596,10 @@ "semver": "bin/semver.js" } }, - "node_modules/@angular-devkit/build-angular/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/@angular-devkit/build-angular/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { @@ -614,30 +635,83 @@ "postcss": "^8.1.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "node_modules/@angular-devkit/build-angular/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" }, "engines": { - "node": ">=14" + "node": ">= 14.15.0" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "typescript": ">=4.9.5" + "webpack": "^5.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, "node_modules/@angular-devkit/build-angular/node_modules/eslint-scope": { @@ -662,20 +736,55 @@ "node": ">=4.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "node_modules/@angular-devkit/build-angular/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=8.6.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@angular-devkit/build-angular/node_modules/https-proxy-agent": { @@ -717,25 +826,13 @@ "node": ">=12.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/inquirer/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "node_modules/@angular-devkit/build-angular/node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">= 10" } }, "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { @@ -744,6 +841,69 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -794,6 +954,30 @@ "webpack": "^5.0.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/sass": { "version": "1.64.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.64.1.tgz", @@ -866,6 +1050,43 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/@angular-devkit/build-angular/node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/webpack": { "version": "5.88.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", @@ -913,26 +1134,191 @@ } } }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1602.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1602.11.tgz", - "integrity": "sha512-2Au6xRMxNugFkXP0LS1TwNE5gAfGW4g6yxC9P5j5p3kdGDnAVaZRTOKB9dg73i3uXtJHUMciYOThV0b78XRxwA==", + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1602.11", + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.2", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.2.tgz", + "integrity": "sha512-1vxKo9+pdSwTOwqPDSYQh84gZYmCJo6OgR5+AZoGLGMZSeqvi9RG5RiUcOMLQYOnuYv0arlhlWxz0ZjyR8ApKw==", + "dev": true, + "peer": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "peer": true + }, + "node_modules/@angular-devkit/core/node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.11.tgz", + "integrity": "sha512-QJ1+ZqVmxhFnIsHnKIA01Ks2ZmzTbctlZT0Wr7cMMFpIhLHlwsMYR+AURRcHJA+s1OBU1jJQfGzTM0s22leVhw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.2.11", + "jsonc-parser": "3.2.0", + "magic-string": "0.30.1", + "ora": "5.4.1", "rxjs": "7.8.1" }, "engines": { "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" } }, - "node_modules/@angular-devkit/core": { + "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { "version": "16.2.11", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.11.tgz", "integrity": "sha512-u3cEQHqhSMWyAFIaPdRukCJwEUJt7Fy3C02gTlTeCB4F/OnftVFIm2e5vmCqMo9rgbfdvjWj9V+7wWiCpMrzAQ==", @@ -959,24 +1345,6 @@ } } }, - "node_modules/@angular-devkit/schematics": { - "version": "16.2.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.11.tgz", - "integrity": "sha512-QJ1+ZqVmxhFnIsHnKIA01Ks2ZmzTbctlZT0Wr7cMMFpIhLHlwsMYR+AURRcHJA+s1OBU1jJQfGzTM0s22leVhw==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "16.2.11", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.1", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, "node_modules/@angular-eslint/bundled-angular-compiler": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.3.1.tgz", @@ -1015,15 +1383,6 @@ "typescript": "*" } }, - "node_modules/@angular-eslint/eslint-plugin-template/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@angular-eslint/template-parser": { "version": "16.3.1", "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-16.3.1.tgz", @@ -1116,6 +1475,48 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular/cli/node_modules/@angular-devkit/architect": { + "version": "0.1602.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1602.11.tgz", + "integrity": "sha512-qC1tPL/82gxqCS1z9pTpLn5NQH6uqbV6UNjbkFEQpTwEyWEK6VLChAJsybHHfbpssPS2HWf31VoUzX7RqDjoQQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.2.11", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/@angular-devkit/core": { + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.11.tgz", + "integrity": "sha512-u3cEQHqhSMWyAFIaPdRukCJwEUJt7Fy3C02gTlTeCB4F/OnftVFIm2e5vmCqMo9rgbfdvjWj9V+7wWiCpMrzAQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular/cli/node_modules/inquirer": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", @@ -1142,22 +1543,21 @@ "node": ">=12.0.0" } }, - "node_modules/@angular/cli/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/@angular/cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@angular/common": { @@ -1268,14 +1668,14 @@ } }, "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -1283,14 +1683,14 @@ } }, "node_modules/@angular/compiler-cli/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1418,104 +1818,40 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz", + "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", - "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", + "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.1", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.5", - "@babel/parser": "^7.23.5", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5", + "@babel/helpers": "^7.24.1", + "@babel/parser": "^7.24.1", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -1531,13 +1867,13 @@ } }, "node_modules/@babel/core/node_modules/@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", "dependencies": { - "@babel/types": "^7.23.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -1545,13 +1881,13 @@ } }, "node_modules/@babel/core/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1624,37 +1960,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1664,9 +1969,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.6.tgz", - "integrity": "sha512-cBXU1vZni/CpGF29iTu4YRbOZt3Wat6zCoMDxRF1MayiEc4URxOj31tT65HUM0CRpMowA3HCJaAOVOUnMf96cw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz", + "integrity": "sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -1674,7 +1979,7 @@ "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -1722,9 +2027,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", + "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1758,13 +2063,13 @@ } }, "node_modules/@babel/helper-function-name/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1794,11 +2099,11 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1835,9 +2140,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "engines": { "node": ">=6.9.0" } @@ -1860,13 +2165,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -1911,9 +2216,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "engines": { "node": ">=6.9.0" } @@ -1949,53 +2254,54 @@ } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", - "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz", + "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==", "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.5", - "@babel/types": "^7.23.5" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -2066,9 +2372,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", - "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2077,12 +2383,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2092,14 +2398,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" + "@babel/plugin-transform-optional-chaining": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -2109,13 +2415,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", - "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2128,6 +2434,7 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", @@ -2146,6 +2453,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", "dependencies": { "@babel/helper-plugin-utils": "^7.18.9", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -2161,6 +2469,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", @@ -2189,6 +2498,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -2277,12 +2587,12 @@ } }, "node_modules/@babel/plugin-syntax-flow": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.23.3.tgz", - "integrity": "sha512-YZiAIpkJAwQXBJLIQbRFayR5c+gJ35Vcz3bg954k7cd73zqjvhacJuL9RbrzPz8qPmZdgqP6EUKwy0PCNhaaPA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.1.tgz", + "integrity": "sha512-sxi2kLTI5DeW5vDtMUsk4mTPwvlUDbjOnoWayhynCwrw4QXRld4QEYwqzY8JmQXaJUtgUuCIurtSRH5sn4c7mA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2292,12 +2602,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2307,12 +2617,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2346,12 +2656,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2463,12 +2773,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", - "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", + "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2494,12 +2804,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2509,13 +2819,13 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz", + "integrity": "sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, @@ -2544,12 +2854,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2559,12 +2869,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz", + "integrity": "sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2574,13 +2884,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2590,13 +2900,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.1.tgz", + "integrity": "sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -2607,17 +2917,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", - "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz", + "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -2629,13 +2939,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2645,26 +2955,26 @@ } }, "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz", + "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2674,13 +2984,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2690,12 +3000,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2705,12 +3015,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -2721,13 +3031,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", "dev": true, "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2737,12 +3047,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -2753,13 +3063,13 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.23.3.tgz", - "integrity": "sha512-26/pQTf9nQSNVJCrLB1IkHUKyPxR+lMrH2QDPG89+Znu9rAMbtrybdbWeE9bb7gzjmE5iXHEY+e0HUwM6Co93Q==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.1.tgz", + "integrity": "sha512-iIYPIWt3dUmUKKE10s3W+jsQ3icFkw0JyRVyY1B7G4yK/nngAOHLVx8xlhA6b/Jzl/Y0nis8gjqhqKtRDQqHWQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-flow": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-flow": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -2769,12 +3079,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", - "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { @@ -2785,14 +3095,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2802,12 +3112,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -2818,12 +3128,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2833,12 +3143,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -2849,12 +3159,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2864,13 +3174,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2880,12 +3190,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-simple-access": "^7.22.5" }, "engines": { @@ -2896,14 +3206,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", - "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { @@ -2914,13 +3224,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2946,12 +3256,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2961,12 +3271,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -2977,12 +3287,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -2993,16 +3303,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", + "integrity": "sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/plugin-transform-parameters": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -3012,13 +3321,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -3028,12 +3337,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -3044,12 +3353,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz", + "integrity": "sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -3061,12 +3370,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", + "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3076,13 +3385,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3092,14 +3401,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz", + "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -3110,12 +3419,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3125,12 +3434,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "regenerator-transform": "^0.15.2" }, "engines": { @@ -3141,12 +3450,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3185,12 +3494,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3200,12 +3509,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { @@ -3216,12 +3525,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3231,12 +3540,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3246,12 +3555,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz", + "integrity": "sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3261,15 +3570,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz", - "integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.1.tgz", + "integrity": "sha512-liYSESjX2fZ7JyBFkYG78nfvHlMKE6IpNdTVnxmlYUR+j5ZLsitFbaAE+eJSK2zPPkNWNw4mXL51rQ8WrvdK0w==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.23.3" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-typescript": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -3279,12 +3588,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3294,13 +3603,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3310,13 +3619,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3326,13 +3635,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -3445,14 +3754,14 @@ } }, "node_modules/@babel/preset-flow": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.23.3.tgz", - "integrity": "sha512-7yn6hl8RIv+KNk6iIrGZ+D06VhVY35wLVf23Cz/mMu1zOr7u4MMP4j0nZ9tLf8+4ZFpnib8cFYgB/oYg9hfswA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.24.1.tgz", + "integrity": "sha512-sWCV2G9pcqZf+JHyv/RyqEIpFypxdCSxWIxQjpdaQxenNog7cN1pr76hg8u0Fz8Qgg0H4ETkGcJnXL8d4j0PPA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-flow-strip-types": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-transform-flow-strip-types": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -3462,9 +3771,9 @@ } }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -3474,20 +3783,20 @@ "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/preset-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", - "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", + "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-typescript": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-syntax-jsx": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-typescript": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -3497,15 +3806,15 @@ } }, "node_modules/@babel/register": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz", - "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.23.7.tgz", + "integrity": "sha512-EjJeB6+kvpk+Y5DAkEAmbOBEFkh9OASx0huoEkqYTFxAZHzOAX2Oh5uwAUuL2rUddqfM0SA+KPXV2TbzoZ2kvQ==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", - "pirates": "^4.0.5", + "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "engines": { @@ -3666,19 +3975,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", - "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.5", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.5", - "@babel/types": "^7.23.5", - "debug": "^4.1.0", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -3686,13 +3995,13 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", - "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", "dependencies": { - "@babel/types": "^7.23.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -3700,9 +4009,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -3938,13 +4247,13 @@ } }, "node_modules/@compodoc/compodoc/node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { @@ -3955,26 +4264,26 @@ } }, "node_modules/@compodoc/compodoc/node_modules/@babel/preset-env": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.5.tgz", - "integrity": "sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.3.tgz", + "integrity": "sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/compat-data": "^7.24.1", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -3986,58 +4295,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.4", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.5", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.3", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.1", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.1", + "@babel/plugin-transform-classes": "^7.24.1", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.1", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.1", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.1", + "@babel/plugin-transform-parameters": "^7.24.1", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.1", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.1", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -4087,93 +4396,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@compodoc/compodoc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/@compodoc/compodoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@compodoc/compodoc/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@compodoc/compodoc/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" }, "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@compodoc/compodoc/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/@compodoc/compodoc/node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", + "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@babel/helper-define-polyfill-provider": "^0.6.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@compodoc/compodoc/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": ">=14.14" - } - }, - "node_modules/@compodoc/compodoc/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@compodoc/compodoc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "node": ">=8.6.0" } }, "node_modules/@compodoc/compodoc/node_modules/jsonc-parser": { @@ -4194,30 +4455,6 @@ "node": ">=12" } }, - "node_modules/@compodoc/compodoc/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@compodoc/compodoc/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@compodoc/compodoc/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -4236,19 +4473,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "node_modules/@compodoc/compodoc/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@compodoc/live-server": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz", @@ -4308,19 +4532,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@compodoc/ngd-core/node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@compodoc/ngd-transformer": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@compodoc/ngd-transformer/-/ngd-transformer-2.1.3.tgz", @@ -4336,20 +4547,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@compodoc/ngd-transformer/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -4424,6 +4621,16 @@ "node": ">=10.12.0" } }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@electron/asar/node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -4453,10 +4660,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@electron/get": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz", - "integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", "dev": true, "dependencies": { "debug": "^4.1.1", @@ -4498,9 +4717,9 @@ } }, "node_modules/@electron/get/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4565,6 +4784,20 @@ "node": ">=12.0.0" } }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", @@ -4605,6 +4838,20 @@ "node": ">=12.13.0" } }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@electron/universal": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", @@ -4645,6 +4892,16 @@ "node": ">= 10" } }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@electron/universal/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -4660,6 +4917,18 @@ "node": ">=10" } }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", @@ -5037,9 +5306,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -5090,10 +5359,20 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5123,6 +5402,18 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -5151,9 +5442,9 @@ "dev": true }, "node_modules/@figspec/components": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.2.tgz", - "integrity": "sha512-rTjjH7wvM55ZuX+MRVPND1cs4Z4JspJvKc9lzGxm/8gD4dLfgeFztQuNy+daGglaxcGXLXTuJ2oJtZ0/lmRKmw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.3.tgz", + "integrity": "sha512-fBwHzJ4ouuOUJEi+yBZIrOy+0/fAjB3AeTcIHTT1PRxLz8P63xwC7R0EsIJXhScIcc+PljGmqbbVJCjLsnaGYA==", "dev": true, "dependencies": { "lit": "^2.1.3" @@ -5173,31 +5464,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", - "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", "dev": true, "dependencies": { - "@floating-ui/utils": "^0.1.1" + "@floating-ui/utils": "^0.2.1" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", - "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", "dev": true, "dependencies": { - "@floating-ui/core": "^1.4.1", - "@floating-ui/utils": "^0.1.1" + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", - "integrity": "sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", "dev": true, "dependencies": { - "@floating-ui/dom": "^1.3.0" + "@floating-ui/dom": "^1.6.1" }, "peerDependencies": { "react": ">=16.8.0", @@ -5205,21 +5496,19 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", - "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==", "dev": true }, "node_modules/@foliojs-fork/fontkit": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.1.tgz", - "integrity": "sha512-U589voc2/ROnvx1CyH9aNzOQWJp127JGU1QAylXGQ7LoEAF6hMmahZLQ4eqAcgHUw+uyW4PjtCItq9qudPkK3A==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz", + "integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==", "dev": true, "dependencies": { "@foliojs-fork/restructure": "^2.0.2", - "brfs": "^2.0.0", "brotli": "^1.2.0", - "browserify-optional": "^1.0.1", "clone": "^1.0.4", "deep-equal": "^1.0.0", "dfa": "^1.2.0", @@ -5228,34 +5517,13 @@ "unicode-trie": "^2.0.0" } }, - "node_modules/@foliojs-fork/fontkit/node_modules/deep-equal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", - "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", - "dev": true, - "dependencies": { - "is-arguments": "^1.1.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.5.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@foliojs-fork/linebreak": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.1.tgz", - "integrity": "sha512-pgY/+53GqGQI+mvDiyprvPWgkTlVBS8cxqee03ejm6gKAQNsR1tCYCIvN9FHy7otZajzMqCgPOgC4cHdt4JPig==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz", + "integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==", "dev": true, "dependencies": { "base64-js": "1.3.1", - "brfs": "^2.0.2", "unicode-trie": "^2.0.0" } }, @@ -5318,6 +5586,28 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -5402,9 +5692,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "dependencies": { "ansi-regex": "^6.0.1" @@ -5511,17 +5801,17 @@ } }, "node_modules/@jest/console": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.5.0.tgz", - "integrity": "sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "peer": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -5529,38 +5819,38 @@ } }, "node_modules/@jest/core": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.5.0.tgz", - "integrity": "sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "peer": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/reporters": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.5.0", - "jest-config": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-resolve-dependencies": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "jest-watcher": "^29.5.0", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -5576,93 +5866,171 @@ } } }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, "node_modules/@jest/environment": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.5.0.tgz", - "integrity": "sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0" + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "peer": true, "dependencies": { - "expect": "^29.5.0", - "jest-snapshot": "^29.5.0" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", - "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "dependencies": { - "jest-get-type": "^29.4.3" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.5.0.tgz", - "integrity": "sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.5.0.tgz", - "integrity": "sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "peer": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/types": "^29.5.0", - "jest-mock": "^29.5.0" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.5.0.tgz", - "integrity": "sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, "peer": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", @@ -5670,13 +6038,13 @@ "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -5694,6 +6062,17 @@ } } }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@jest/reporters/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -5715,26 +6094,56 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@jest/schemas": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", - "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "dependencies": { - "@sinclair/typebox": "^0.25.16" + "@sinclair/typebox": "^0.27.8" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/source-map": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.4.3.tgz", - "integrity": "sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "peer": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.15", + "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" }, @@ -5743,14 +6152,14 @@ } }, "node_modules/@jest/test-result": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.5.0.tgz", - "integrity": "sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "peer": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -5759,15 +6168,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz", - "integrity": "sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "peer": true, "dependencies": { - "@jest/test-result": "^29.5.0", + "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "slash": "^3.0.0" }, "engines": { @@ -5775,22 +6184,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz", - "integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.5.0", - "@jridgewell/trace-mapping": "^0.3.15", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -5807,12 +6216,12 @@ "dev": true }, "node_modules/@jest/types": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", - "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", @@ -5824,42 +6233,42 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -5868,19 +6277,14 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", @@ -5916,21 +6320,6 @@ "node": ">= 12" } }, - "node_modules/@koa/router/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -5944,9 +6333,9 @@ "dev": true }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", - "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", + "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==", "dev": true }, "node_modules/@lit/reactive-element": { @@ -6029,6 +6418,15 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6060,6 +6458,17 @@ "node": ">= 6" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -6112,6 +6521,40 @@ "@msgpack/msgpack": "^2.7.0" } }, + "node_modules/@microsoft/signalr/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/@microsoft/signalr/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@msgpack/msgpack": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", @@ -6120,6 +6563,22 @@ "node": ">= 10" } }, + "node_modules/@napi-rs/cli": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.16.2.tgz", + "integrity": "sha512-U2aZfnr0s9KkXpZlYC0l5WxWCXL7vJUNpCnWMwq3T9GG9rhYAAUM9CTZsi1Z+0iR2LcHbfq9EfMgoqnuTyUjfg==", + "dev": true, + "bin": { + "napi": "scripts/index.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@ndelangen/get-tarball": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz", @@ -6200,15 +6659,16 @@ } }, "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "dev": true, "dependencies": { + "@gar/promisify": "^1.1.3", "semver": "^7.3.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/git": { @@ -6284,6 +6744,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/move-file/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@npmcli/move-file/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -6304,6 +6774,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@npmcli/move-file/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@npmcli/move-file/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -7089,10 +7571,37 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@schematics/angular/node_modules/@angular-devkit/core": { + "version": "16.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.11.tgz", + "integrity": "sha512-u3cEQHqhSMWyAFIaPdRukCJwEUJt7Fy3C02gTlTeCB4F/OnftVFIm2e5vmCqMo9rgbfdvjWj9V+7wWiCpMrzAQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "picomatch": "2.3.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "dev": true, "dependencies": { "@hapi/hoek": "^9.0.0" @@ -7145,6 +7654,53 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@sigstore/sign/node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/cacache": { + "version": "17.1.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", + "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@sigstore/sign/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -7193,6 +7749,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@sigstore/sign/node_modules/make-fetch-happen/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@sigstore/sign/node_modules/minipass-fetch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", @@ -7210,13 +7775,40 @@ "encoding": "^0.1.13" } }, - "node_modules/@sigstore/sign/node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "node_modules/@sigstore/sign/node_modules/ssri": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", + "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/@sigstore/tuf": { @@ -7233,9 +7825,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.25.24", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", - "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, "node_modules/@sindresorhus/is": { @@ -7250,19 +7842,31 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz", - "integrity": "sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -7300,32 +7904,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/addon-actions/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-actions/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@storybook/addon-backgrounds": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.17.tgz", @@ -7418,126 +7996,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/preview-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", - "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.17", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/theming": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.17.tgz", - "integrity": "sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==", - "dev": true, - "dependencies": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.17", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/addon-docs/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-docs/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@storybook/addon-essentials": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-7.6.17.tgz", @@ -7568,153 +8026,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/manager-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.17.tgz", - "integrity": "sha512-IJIV1Yc6yw1dhCY4tReHCfBnUKDqEBnMyHp3mbXpsaHxnxJZrXO45WjRAZIKlQKhl/Ge1CrnznmHRCmYgqmrWg==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/router": "7.6.17", - "@storybook/theming": "7.6.17", - "@storybook/types": "7.6.17", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "store2": "^2.14.2", - "telejson": "^7.2.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/preview-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", - "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.17", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/router": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.17.tgz", - "integrity": "sha512-GnyC0j6Wi5hT4qRhSyT8NPtJfGmf82uZw97LQRWeyYu5gWEshUdM7aj40XlNiScd5cZDp0owO1idduVF2k2l2A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "memoizerific": "^1.11.3", - "qs": "^6.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/theming": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.17.tgz", - "integrity": "sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==", - "dev": true, - "dependencies": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.17", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/addon-highlight": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.6.17.tgz", @@ -7745,104 +8056,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/addon-interactions/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@storybook/addon-interactions/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-interactions/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-interactions/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-interactions/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/addon-interactions/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@storybook/addon-interactions/node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/@storybook/addon-links": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-7.6.17.tgz", @@ -7918,23 +8131,19 @@ } }, "node_modules/@storybook/addons": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-7.2.2.tgz", - "integrity": "sha512-yWBBpBcRyPP1deAjzWV9OAXrPfeRd/vRpJw09dWHzuD3xtnd3jZ2h+t1r9a5yTSQbP5GO1YdS/WOK5Uf9hcsuw==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-7.6.17.tgz", + "integrity": "sha512-Ok18Y698Ccyg++MoUNJNHY0cXUvo8ETFIRLJk1g9ElJ70j6kPgNnzW2pAtZkBNmswHtofZ7pT156cj96k/LgfA==", "dev": true, "peer": true, "dependencies": { - "@storybook/manager-api": "7.2.2", - "@storybook/preview-api": "7.2.2", - "@storybook/types": "7.2.2" + "@storybook/manager-api": "7.6.17", + "@storybook/preview-api": "7.6.17", + "@storybook/types": "7.6.17" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@storybook/angular": { @@ -8000,92 +8209,6 @@ } } }, - "node_modules/@storybook/angular/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/angular/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/angular/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/angular/node_modules/@storybook/preview-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", - "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.17", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/angular/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/blocks": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.17.tgz", @@ -8125,153 +8248,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@storybook/blocks/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks/node_modules/@storybook/manager-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.17.tgz", - "integrity": "sha512-IJIV1Yc6yw1dhCY4tReHCfBnUKDqEBnMyHp3mbXpsaHxnxJZrXO45WjRAZIKlQKhl/Ge1CrnznmHRCmYgqmrWg==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/router": "7.6.17", - "@storybook/theming": "7.6.17", - "@storybook/types": "7.6.17", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "store2": "^2.14.2", - "telejson": "^7.2.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks/node_modules/@storybook/preview-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", - "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.17", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks/node_modules/@storybook/router": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.17.tgz", - "integrity": "sha512-GnyC0j6Wi5hT4qRhSyT8NPtJfGmf82uZw97LQRWeyYu5gWEshUdM7aj40XlNiScd5cZDp0owO1idduVF2k2l2A==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "memoizerific": "^1.11.3", - "qs": "^6.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks/node_modules/@storybook/theming": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.17.tgz", - "integrity": "sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==", - "dev": true, - "dependencies": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.17", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/blocks/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/builder-manager": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.17.tgz", @@ -8300,20 +8276,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/builder-manager/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@storybook/builder-webpack5": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.6.17.tgz", @@ -8369,7 +8331,19 @@ } } }, - "node_modules/@storybook/builder-webpack5/node_modules/@storybook/channels": { + "node_modules/@storybook/builder-webpack5/node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@storybook/channels": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", @@ -8387,119 +8361,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/builder-webpack5/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/@storybook/preview-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", - "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.17", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/builder-webpack5/node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/channels": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.2.2.tgz", - "integrity": "sha512-FkH5QzKkq7smtPlaKTWalJ+sN13H4dWtxdZ+ePFAXaubsBqGqo3Dw3e/hrbjrMqFjTwiKnmj5K5bjhdJcvzi1A==", - "dev": true, - "peer": true, - "dependencies": { - "@storybook/client-logger": "7.2.2", - "@storybook/core-events": "7.2.2", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.0.3", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/cli": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.17.tgz", @@ -8557,13 +8418,13 @@ } }, "node_modules/@storybook/cli/node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { @@ -8574,26 +8435,26 @@ } }, "node_modules/@storybook/cli/node_modules/@babel/preset-env": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.6.tgz", - "integrity": "sha512-2XPn/BqKkZCpzYhUUNZ1ssXw7DcXfKQEjv/uXZUXgaebCMYmkEsfZ2yY+vv+xtXv50WmL5SGhyB6/xsWxIvvOQ==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.3.tgz", + "integrity": "sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", + "@babel/compat-data": "^7.24.1", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -8605,58 +8466,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.4", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.5", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.1", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.1", + "@babel/plugin-transform-classes": "^7.24.1", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.1", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.1", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.1", + "@babel/plugin-transform-parameters": "^7.24.1", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.1", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.1", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -8690,64 +8551,29 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@storybook/cli/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", + "node_modules/@storybook/cli/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@storybook/cli/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", + "node_modules/@storybook/cli/node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", + "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", "dev": true, "dependencies": { - "@storybook/global": "^5.0.0" + "@babel/helper-define-polyfill-provider": "^0.6.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/cli/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@storybook/cli/node_modules/commander": { @@ -8759,20 +8585,6 @@ "node": ">= 6" } }, - "node_modules/@storybook/cli/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@storybook/cli/node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -8789,11 +8601,10 @@ } }, "node_modules/@storybook/client-logger": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.2.2.tgz", - "integrity": "sha512-ULqPNTJsJdlWTQt5V/hEv4CUq7GgrLzLvcjhKB9IYbp4a0gjhinfq7jBFIcXRE8BSOQLui2PDGE3SzElzOp5/g==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", + "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", "dev": true, - "peer": true, "dependencies": { "@storybook/global": "^5.0.0" }, @@ -8829,13 +8640,13 @@ } }, "node_modules/@storybook/codemod/node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { @@ -8846,26 +8657,26 @@ } }, "node_modules/@storybook/codemod/node_modules/@babel/preset-env": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", - "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.3.tgz", + "integrity": "sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", + "@babel/compat-data": "^7.24.1", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -8877,58 +8688,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.1", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.1", + "@babel/plugin-transform-classes": "^7.24.1", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.1", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.1", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.1", + "@babel/plugin-transform-parameters": "^7.24.1", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.1", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.1", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -8953,74 +8764,26 @@ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@storybook/codemod/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/codemod/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@storybook/codemod/node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", + "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -9076,86 +8839,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@storybook/components/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/components/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/components/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/components/node_modules/@storybook/theming": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.17.tgz", - "integrity": "sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==", - "dev": true, - "dependencies": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.17", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/components/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/core-common": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.17.tgz", @@ -9191,38 +8874,7 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/core-common/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-common/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-common/node_modules/@storybook/core-events": { + "node_modules/@storybook/core-events": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", @@ -9235,93 +8887,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/core-common/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-common/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@storybook/core-common/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/core-common/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/core-common/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/core-events": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.2.2.tgz", - "integrity": "sha512-0MUsOygFSyYRIWHrVAA7Y7zBoehdILgK2AbnV42qescmPD48YyovkSRiGq0BwSWvuvMRq+094dp7sh2tkfSGHg==", - "dev": true, - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/core-server": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.17.tgz", @@ -9375,127 +8940,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/core-server/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/preview-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", - "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.17", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/core-server/node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@storybook/core-webpack": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.6.17.tgz", @@ -9513,70 +8957,10 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/core-webpack/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-webpack/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-webpack/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-webpack/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/csf": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.2.tgz", - "integrity": "sha512-ePrvE/pS1vsKR9Xr+o+YwdqNgHUyXvg+1Xjx0h9LrVx7Zq4zNe06pd63F5EvzTbCbJsHj7GHr9tkiaqm7U8WRA==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.3.tgz", + "integrity": "sha512-IPZvXXo4b3G+gpmgBSBqVM81jbp2ePOKsvhgJdhyZJtkYQCII7rg9KKLQhvBQM5sLaF1eU6r0iuwmyynC9d9SA==", "dev": true, "dependencies": { "type-fest": "^2.19.0" @@ -9618,94 +9002,20 @@ } }, "node_modules/@storybook/csf-tools/node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@storybook/csf-tools/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf-tools/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf-tools/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf-tools/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf-tools/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@storybook/docs-mdx": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@storybook/docs-mdx/-/docs-mdx-0.1.0.tgz", @@ -9731,92 +9041,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/docs-tools/node_modules/@storybook/channels": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.17.tgz", - "integrity": "sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/docs-tools/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/docs-tools/node_modules/@storybook/core-events": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.17.tgz", - "integrity": "sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/docs-tools/node_modules/@storybook/preview-api": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", - "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@storybook/client-logger": "7.6.17", - "@storybook/core-events": "7.6.17", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.17", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/docs-tools/node_modules/@storybook/types": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", - "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.17", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/expect": { "version": "28.1.3-5", "resolved": "https://registry.npmjs.org/@storybook/expect/-/expect-28.1.3-5.tgz", @@ -9929,6 +9153,12 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/@storybook/expect/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@storybook/global": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", @@ -9959,22 +9189,6 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@storybook/jest/node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/@storybook/jest/node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -9991,15 +9205,6 @@ "pretty-format": "^28.0.0" } }, - "node_modules/@storybook/jest/node_modules/@types/yargs": { - "version": "16.0.9", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", - "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/@storybook/jest/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -10060,19 +9265,6 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@storybook/jest/node_modules/jest-mock": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", - "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", - "dev": true, - "dependencies": { - "@jest/types": "^27.5.1", - "@types/node": "*" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/@storybook/jest/node_modules/pretty-format": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", @@ -10088,6 +9280,12 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/@storybook/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@storybook/manager": { "version": "7.6.17", "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.17.tgz", @@ -10099,35 +9297,29 @@ } }, "node_modules/@storybook/manager-api": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.2.2.tgz", - "integrity": "sha512-7EI7TABGGB3VOTc4q7byC5dW/9A1xUJyR1gfCPU+7XiSNItnCz+seBJMSaf6Em/9wYxSAL6PQAGhrwTHGzgWAA==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.17.tgz", + "integrity": "sha512-IJIV1Yc6yw1dhCY4tReHCfBnUKDqEBnMyHp3mbXpsaHxnxJZrXO45WjRAZIKlQKhl/Ge1CrnznmHRCmYgqmrWg==", "dev": true, - "peer": true, "dependencies": { - "@storybook/channels": "7.2.2", - "@storybook/client-logger": "7.2.2", - "@storybook/core-events": "7.2.2", - "@storybook/csf": "^0.1.0", + "@storybook/channels": "7.6.17", + "@storybook/client-logger": "7.6.17", + "@storybook/core-events": "7.6.17", + "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", - "@storybook/router": "7.2.2", - "@storybook/theming": "7.2.2", - "@storybook/types": "7.2.2", + "@storybook/router": "7.6.17", + "@storybook/theming": "7.6.17", + "@storybook/types": "7.6.17", "dequal": "^2.0.2", "lodash": "^4.17.21", "memoizerific": "^1.11.3", - "semver": "^7.3.7", "store2": "^2.14.2", - "telejson": "^7.0.3", + "telejson": "^7.2.0", "ts-dedent": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@storybook/mdx2-csf": { @@ -10167,18 +9359,17 @@ } }, "node_modules/@storybook/preview-api": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.2.2.tgz", - "integrity": "sha512-II0EioQCGS2zTSoHbXNKyI1rwk2X7MBi2/tJerj4w4Qwi2fDQlwM0LKsIWlRjXTxBpOAsOoTelh24wSBoZu4bg==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.17.tgz", + "integrity": "sha512-wLfDdI9RWo1f2zzFe54yRhg+2YWyxLZvqdZnSQ45mTs4/7xXV5Wfbv3QNTtcdw8tT3U5KRTrN1mTfTCiRJc0Kw==", "dev": true, - "peer": true, "dependencies": { - "@storybook/channels": "7.2.2", - "@storybook/client-logger": "7.2.2", - "@storybook/core-events": "7.2.2", - "@storybook/csf": "^0.1.0", + "@storybook/channels": "7.6.17", + "@storybook/client-logger": "7.6.17", + "@storybook/core-events": "7.6.17", + "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", - "@storybook/types": "7.2.2", + "@storybook/types": "7.6.17", "@types/qs": "^6.9.5", "dequal": "^2.0.2", "lodash": "^4.17.21", @@ -10208,23 +9399,18 @@ } }, "node_modules/@storybook/router": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.2.2.tgz", - "integrity": "sha512-cnJg43dm3dVifKkRBUsQ4wXC4sJOm46JAS95yRPeGACoHpJTcbCWk1n5GLYA7ozO+KFQSNdxHxPIjNqvnzMFiA==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.17.tgz", + "integrity": "sha512-GnyC0j6Wi5hT4qRhSyT8NPtJfGmf82uZw97LQRWeyYu5gWEshUdM7aj40XlNiScd5cZDp0owO1idduVF2k2l2A==", "dev": true, - "peer": true, "dependencies": { - "@storybook/client-logger": "7.2.2", + "@storybook/client-logger": "7.6.17", "memoizerific": "^1.11.3", "qs": "^6.10.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/@storybook/telemetry": { @@ -10247,33 +9433,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/telemetry/node_modules/@storybook/client-logger": { - "version": "7.6.17", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.17.tgz", - "integrity": "sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/telemetry/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@storybook/testing-library": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@storybook/testing-library/-/testing-library-0.2.2.tgz", @@ -10286,14 +9445,13 @@ } }, "node_modules/@storybook/theming": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.2.2.tgz", - "integrity": "sha512-B4nxcxl4IyVvB1NXwRi4yopAS73zl052f2zJi3kVghJbZ3tgPwgvi3CVpOs2D4pgmxOrKCgiLnzLrGTH+13+0A==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.17.tgz", + "integrity": "sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==", "dev": true, - "peer": true, "dependencies": { "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.2.2", + "@storybook/client-logger": "7.6.17", "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3" }, @@ -10307,13 +9465,12 @@ } }, "node_modules/@storybook/types": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.2.2.tgz", - "integrity": "sha512-yrL0+KD+dsusQvDmNKQGv36WjvdhoiUxMDEBgsZkP047kRc3b8/zEbD3Tu7iMDsWnpgwip1Frgy29Ro3UtK57Q==", + "version": "7.6.17", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.17.tgz", + "integrity": "sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==", "dev": true, - "peer": true, "dependencies": { - "@storybook/channels": "7.2.2", + "@storybook/channels": "7.6.17", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -10324,13 +9481,13 @@ } }, "node_modules/@swc/core": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.100.tgz", - "integrity": "sha512-7dKgTyxJjlrMwFZYb1auj3Xq0D8ZBe+5oeIgfMlRU05doXZypYJe0LAk0yjj3WdbwYzpF+T1PLxwTWizI0pckw==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.8.tgz", + "integrity": "sha512-uY2RSJcFPgNOEg12RQZL197LZX+MunGiKxsbxmh22VfVxrOYGRvh4mPANFlrD1yb38CgmW1wI6YgIi8LkIwmWg==", "dev": true, "hasInstallScript": true, "dependencies": { - "@swc/counter": "^0.1.1", + "@swc/counter": "^0.1.2", "@swc/types": "^0.1.5" }, "engines": { @@ -10341,15 +9498,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.100", - "@swc/core-darwin-x64": "1.3.100", - "@swc/core-linux-arm64-gnu": "1.3.100", - "@swc/core-linux-arm64-musl": "1.3.100", - "@swc/core-linux-x64-gnu": "1.3.100", - "@swc/core-linux-x64-musl": "1.3.100", - "@swc/core-win32-arm64-msvc": "1.3.100", - "@swc/core-win32-ia32-msvc": "1.3.100", - "@swc/core-win32-x64-msvc": "1.3.100" + "@swc/core-darwin-arm64": "1.4.8", + "@swc/core-darwin-x64": "1.4.8", + "@swc/core-linux-arm-gnueabihf": "1.4.8", + "@swc/core-linux-arm64-gnu": "1.4.8", + "@swc/core-linux-arm64-musl": "1.4.8", + "@swc/core-linux-x64-gnu": "1.4.8", + "@swc/core-linux-x64-musl": "1.4.8", + "@swc/core-win32-arm64-msvc": "1.4.8", + "@swc/core-win32-ia32-msvc": "1.4.8", + "@swc/core-win32-x64-msvc": "1.4.8" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -10361,9 +9519,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.100.tgz", - "integrity": "sha512-XVWFsKe6ei+SsDbwmsuRkYck1SXRpO60Hioa4hoLwR8fxbA9eVp6enZtMxzVVMBi8ej5seZ4HZQeAWepbukiBw==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.8.tgz", + "integrity": "sha512-hhQCffRTgzpTIbngSnC30vV6IJVTI9FFBF954WEsshsecVoCGFiMwazBbrkLG+RwXENTrMhgeREEFh6R3KRgKQ==", "cpu": [ "arm64" ], @@ -10377,9 +9535,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.100.tgz", - "integrity": "sha512-KF/MXrnH1nakm1wbt4XV8FS7kvqD9TGmVxeJ0U4bbvxXMvzeYUurzg3AJUTXYmXDhH/VXOYJE5N5RkwZZPs5iA==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.8.tgz", + "integrity": "sha512-P3ZBw8Jr8rKhY/J8d+6WqWriqngGTgHwtFeJ8MIakQJTbdYbFgXSZxcvDiERg3psbGeFXaUaPI0GO6BXv9k/OQ==", "cpu": [ "x64" ], @@ -10392,10 +9550,26 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.8.tgz", + "integrity": "sha512-PP9JIJt19bUWhAGcQW6qMwTjZOcMyzkvZa0/LWSlDm0ORYVLmDXUoeQbGD3e0Zju9UiZxyulnpjEN0ZihJgPTA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.100.tgz", - "integrity": "sha512-p8hikNnAEJrw5vHCtKiFT4hdlQxk1V7vqPmvUDgL/qe2menQDK/i12tbz7/3BEQ4UqUPnvwpmVn2d19RdEMNxw==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.8.tgz", + "integrity": "sha512-HvEWnwKHkoVUr5iftWirTApFJ13hGzhAY2CMw4lz9lur2m+zhPviRRED0FCI6T95Knpv7+8eUOr98Z7ctrG6DQ==", "cpu": [ "arm64" ], @@ -10409,9 +9583,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.100.tgz", - "integrity": "sha512-BWx/0EeY89WC4q3AaIaBSGfQxkYxIlS3mX19dwy2FWJs/O+fMvF9oLk/CyJPOZzbp+1DjGeeoGFuDYpiNO91JA==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.8.tgz", + "integrity": "sha512-kY8+qa7k/dEeBq9p0Hrta18QnJPpsiJvDQSLNaTIFpdM3aEM9zbkshWz8gaX5VVGUEALowCBUWqmzO4VaqM+2w==", "cpu": [ "arm64" ], @@ -10425,9 +9599,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.100.tgz", - "integrity": "sha512-XUdGu3dxAkjsahLYnm8WijPfKebo+jHgHphDxaW0ovI6sTdmEGFDew7QzKZRlbYL2jRkUuuKuDGvD6lO5frmhA==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.8.tgz", + "integrity": "sha512-0WWyIw432wpO/zeGblwq4f2YWam4pn8Z/Ig4KzHMgthR/KmiLU3f0Z7eo45eVmq5vcU7Os1zi/Zb65OOt09q/w==", "cpu": [ "x64" ], @@ -10441,9 +9615,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.100.tgz", - "integrity": "sha512-PhoXKf+f0OaNW/GCuXjJ0/KfK9EJX7z2gko+7nVnEA0p3aaPtbP6cq1Ubbl6CMoPL+Ci3gZ7nYumDqXNc3CtLQ==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.8.tgz", + "integrity": "sha512-p4yxvVS05rBNCrBaSTa20KK88vOwtg8ifTW7ec/yoab0bD5EwzzB8KbDmLLxE6uziFa0sdjF0dfRDwSZPex37Q==", "cpu": [ "x64" ], @@ -10457,9 +9631,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.100.tgz", - "integrity": "sha512-PwLADZN6F9cXn4Jw52FeP/MCLVHm8vwouZZSOoOScDtihjY495SSjdPnlosMaRSR4wJQssGwiD/4MbpgQPqbAw==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.8.tgz", + "integrity": "sha512-jKuXihxAaqUnbFfvPxtmxjdJfs87F1GdBf33il+VUmSyWCP4BE6vW+/ReDAe8sRNsKyrZ3UH1vI5q1n64csBUA==", "cpu": [ "arm64" ], @@ -10473,9 +9647,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.100.tgz", - "integrity": "sha512-0f6nicKSLlDKlyPRl2JEmkpBV4aeDfRQg6n8mPqgL7bliZIcDahG0ej+HxgNjZfS3e0yjDxsNRa6sAqWU2Z60A==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.8.tgz", + "integrity": "sha512-O0wT4AGHrX8aBeH6c2ADMHgagAJc5Kf6W48U5moyYDAkkVnKvtSc4kGhjWhe1Yl0sI0cpYh2In2FxvYsb44eWw==", "cpu": [ "ia32" ], @@ -10489,9 +9663,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.100", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.100.tgz", - "integrity": "sha512-b7J0rPoMkRTa3XyUGt8PwCaIBuYWsL2DqbirrQKRESzgCvif5iNpqaM6kjIjI/5y5q1Ycv564CB51YDpiS8EtQ==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.8.tgz", + "integrity": "sha512-C2AYc3A2o+ECciqsJWRgIpp83Vk5EaRzHe7ed/xOWzVd0MsWR+fweEsyOjlmzHfpUxJSi46Ak3/BIZJlhZbXbg==", "cpu": [ "x64" ], @@ -10505,16 +9679,19 @@ } }, "node_modules/@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true }, "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz", + "integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", @@ -10529,9 +9706,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", - "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -10547,50 +9724,59 @@ "node": ">=14" } }, - "node_modules/@testing-library/dom/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@testing-library/dom/node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/@testing-library/dom/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, "node_modules/@testing-library/jest-dom": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.5.tgz", - "integrity": "sha512-3y04JLW+EceVPy2Em3VwNr95dOKqA8DhR0RJHhHKDZNYXcVXnEK7WIrpj4eYU8SVt/qYZ2aRWt/WgQ+grNES8g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", + "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", "dev": true, "dependencies": { - "@adobe/css-tools": "^4.3.1", + "@adobe/css-tools": "^4.3.2", "@babel/runtime": "^7.9.2", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", + "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.15", "redent": "^3.0.0" }, @@ -10601,6 +9787,7 @@ }, "peerDependencies": { "@jest/globals": ">= 28", + "@types/bun": "latest", "@types/jest": ">= 28", "jest": ">= 28", "vitest": ">= 0.32" @@ -10609,6 +9796,9 @@ "@jest/globals": { "optional": true }, + "@types/bun": { + "optional": true + }, "@types/jest": { "optional": true }, @@ -10633,10 +9823,16 @@ "node": ">=8" } }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, "node_modules/@testing-library/user-event": { - "version": "14.5.1", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.1.tgz", - "integrity": "sha512-UCcUKrUYGj7ClomOo2SpNVvx4/fkd/2BbIHDCle8A0ax+P3bU7yJwDBDrS6ZwdTMARWTGODX1hEsCcO+7beJjg==", + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", "dev": true, "engines": { "node": ">=12", @@ -10687,15 +9883,6 @@ "path-browserify": "^1.0.1" } }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/@ts-morph/common/node_modules/minimatch": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", @@ -10748,34 +9935,10 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@types/accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -10794,9 +9957,9 @@ "dev": true }, "node_modules/@types/babel__core": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", - "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -10807,18 +9970,18 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -10826,18 +9989,18 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.0.tgz", - "integrity": "sha512-TBOjqAGf0hmaqRwpii5LLkJLg7c6OMm4nHLmpsUxwk9bBHtoTC6dAHdVWdGv4TBxj2CZOZY8Xfq8WmfoVi7n4Q==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, "dependencies": { "@types/connect": "*", @@ -10845,9 +10008,9 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", - "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -10876,18 +10039,18 @@ } }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, "dependencies": { "@types/express-serve-static-core": "*", @@ -10895,15 +10058,15 @@ } }, "node_modules/@types/content-disposition": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.5.tgz", - "integrity": "sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==", + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", "dev": true }, "node_modules/@types/cookies": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", - "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", + "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", "dev": true, "dependencies": { "@types/connect": "*", @@ -10922,18 +10085,18 @@ } }, "node_modules/@types/debug": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", - "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "dev": true, "dependencies": { "@types/ms": "*" } }, "node_modules/@types/detect-port": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.3.tgz", - "integrity": "sha512-bV/jQlAJ/nPY3XqSatkGpu+nGzou+uSwrH1cROhn+jBFg47yaNH+blW4C7p9KhopC7QxCv/6M86s37k8dMk0Yg==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.5.tgz", + "integrity": "sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==", "dev": true }, "node_modules/@types/doctrine": { @@ -10955,15 +10118,15 @@ "dev": true }, "node_modules/@types/emscripten": { - "version": "1.39.7", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.7.tgz", - "integrity": "sha512-tLqYV94vuqDrXh515F/FOGtBcRMTPGvVV1LzLbtYDcQmmhtpf/gLYf+hikBbQk8MzOHNz37wpFfJbYAuSn8HqA==", + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", + "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", "dev": true }, "node_modules/@types/eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==", + "version": "8.56.6", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.6.tgz", + "integrity": "sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==", "dev": true, "dependencies": { "@types/estree": "*", @@ -10971,9 +10134,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "dependencies": { "@types/eslint": "*", @@ -10981,9 +10144,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, "node_modules/@types/expect": { @@ -10993,9 +10156,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.17", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", - "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -11005,9 +10168,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.35", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", - "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "version": "4.17.43", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", + "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "dev": true, "dependencies": { "@types/node": "*", @@ -11017,18 +10180,18 @@ } }, "node_modules/@types/filesystem": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.32.tgz", - "integrity": "sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==", + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", "dev": true, "dependencies": { "@types/filewriter": "*" } }, "node_modules/@types/filewriter": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.29.tgz", - "integrity": "sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", "dev": true }, "node_modules/@types/find-cache-dir": { @@ -11053,18 +10216,18 @@ } }, "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/har-format": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.10.tgz", - "integrity": "sha512-o0J30wqycjF5miWDKYKKzzOU1ZTLuA42HZ4HE7/zqTOc/jTLdQ5NhYWvsRQo45Nfi1KHoRdNhteSI4BAxTF1Pg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.15.tgz", + "integrity": "sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==", "dev": true }, "node_modules/@types/html-minifier-terser": { @@ -11074,27 +10237,27 @@ "dev": true }, "node_modules/@types/http-assert": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", - "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", "dev": true }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true }, "node_modules/@types/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.11", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", - "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dev": true, "dependencies": { "@types/node": "*" @@ -11111,24 +10274,24 @@ } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" @@ -11144,6 +10307,38 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@types/jquery": { "version": "3.5.29", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", @@ -11165,9 +10360,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/json5": { @@ -11177,9 +10372,9 @@ "dev": true }, "node_modules/@types/keygrip": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", "dev": true }, "node_modules/@types/keyv": { @@ -11235,9 +10430,9 @@ } }, "node_modules/@types/koa-compose": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", - "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", "dev": true, "dependencies": { "@types/koa": "*" @@ -11253,9 +10448,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.14.195", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", - "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", "dev": true }, "node_modules/@types/lowdb": { @@ -11274,42 +10469,42 @@ "dev": true }, "node_modules/@types/mdast": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", - "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", "dev": true, "dependencies": { - "@types/unist": "*" + "@types/unist": "^2" } }, "node_modules/@types/mdx": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.6.tgz", - "integrity": "sha512-sVcwEG10aFU2KcM7cIA0M410UPv/DesOPyG8zMVk0QUDexHA3lYmGucpEpZ2dtWWhi2ip3CG+5g/iH0PwoW4Fw==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.12.tgz", + "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==", "dev": true }, "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, "node_modules/@types/mime-types": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", - "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", "dev": true }, "node_modules/@types/ms": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", - "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", "dev": true }, "node_modules/@types/node": { - "version": "18.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.19.tgz", - "integrity": "sha512-qqV6hSy9zACEhQUy5CEGeuXAZN0fNjqLWRIvOXOwdFYhFoKBiY08VKR5kgchr90+TitLVhpUEb54hk4bYaArUw==", + "version": "18.19.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.29.tgz", + "integrity": "sha512-5pAX7ggTmWZdhUrhRWLPf+5oM7F80bcKVCBbr0zwEkTNzTJL2CWQjznpFgHYy6GrzkYi2Yjy7DHKoynFxqPV8g==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -11358,9 +10553,9 @@ } }, "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, "node_modules/@types/papaparse": { @@ -11373,15 +10568,15 @@ } }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "dev": true }, "node_modules/@types/plist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.2.tgz", - "integrity": "sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", "dev": true, "optional": true, "dependencies": { @@ -11389,23 +10584,16 @@ "xmlbuilder": ">=11.0.1" } }, - "node_modules/@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true, - "peer": true - }, "node_modules/@types/pretty-hrtime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz", - "integrity": "sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==", "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "dev": true }, "node_modules/@types/proper-lockfile": { @@ -11418,15 +10606,15 @@ } }, "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", + "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, "node_modules/@types/react": { @@ -11441,18 +10629,18 @@ } }, "node_modules/@types/react-dom": { - "version": "16.9.19", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.19.tgz", - "integrity": "sha512-xC8D280Bf6p0zguJ8g62jcEOKZiUbx9sIe6O3tT/lKfR87A7A6g65q13z6D5QUMIa/6yFPkNhqjF5z/VVZEYqQ==", + "version": "16.9.24", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.24.tgz", + "integrity": "sha512-Gcmq2JTDheyWn/1eteqyzzWKSqDjYU6KYsIvH7thb7CR5OYInAWOX+7WnKf6PaU/cbdOc4szJItcDEJO7UGmfA==", "dev": true, "dependencies": { "@types/react": "^16" } }, "node_modules/@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "dependencies": { "@types/node": "*" @@ -11465,21 +10653,21 @@ "dev": true }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", "dev": true }, "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", - "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, "dependencies": { "@types/mime": "^1", @@ -11487,82 +10675,83 @@ } }, "node_modules/@types/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", - "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "dev": true, "dependencies": { + "@types/http-errors": "*", "@types/mime": "*", "@types/node": "*" } }, "node_modules/@types/sizzle": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", "dev": true }, "node_modules/@types/sockjs": { - "version": "0.3.33", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", - "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, "node_modules/@types/through": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz", - "integrity": "sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==", + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, "node_modules/@types/trusted-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", - "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, "node_modules/@types/unist": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", "dev": true }, "node_modules/@types/uuid": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", - "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, "node_modules/@types/verror": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", - "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", + "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", "dev": true, "optional": true }, @@ -11577,30 +10766,39 @@ } }, "node_modules/@types/webpack-env": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.1.tgz", - "integrity": "sha512-D0HJET2/UY6k9L6y3f5BL+IDxZmPkYmPT4+qBrRdmRLYRuV0qNKizMgTvYxXZYn+36zjPeoDZAEYBCM6XB+gww==", + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.4.tgz", + "integrity": "sha512-I6e+9+HtWADAWeeJWDFQtdk4EVSAbj6Rtz4q8fJ7mSr1M0jzlFcs8/HZ+Xb5SHzVm1dxH7aUiI+A8kA8Gcrm0A==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, "node_modules/@types/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "optional": true, "dependencies": { @@ -11614,16 +10812,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -11632,15 +10830,15 @@ "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -11648,84 +10846,26 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -11734,78 +10874,37 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.59.7.tgz", - "integrity": "sha512-jqM0Cjfvta/sBlY1MxdXYv853/dJUC2wmUWnKoG2srwp0njNGQ6Zu/XLWoRFiLvocQbzBbpHkPFwKgC2UqyovA==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "5.59.7" + "@typescript-eslint/utils": "5.62.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -11818,75 +10917,27 @@ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/utils": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.7.tgz", - "integrity": "sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.7", - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/typescript-estree": "5.59.7", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/experimental-utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -11894,116 +10945,17 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz", - "integrity": "sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12095,12 +11047,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz", - "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12108,21 +11060,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz", - "integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", - "@typescript-eslint/visitor-keys": "5.59.7", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12257,16 +11210,16 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.7", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz", - "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.59.7", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "7.4.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -12292,9 +11245,9 @@ } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -12314,9 +11267,9 @@ "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { @@ -12337,15 +11290,15 @@ "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -12373,28 +11326,28 @@ "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -12402,24 +11355,24 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -12428,12 +11381,12 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -12519,34 +11472,6 @@ "node": ">= 6" } }, - "node_modules/@wessberg/ts-evaluator/node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, "node_modules/@wessberg/ts-evaluator/node_modules/cssstyle": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", @@ -12579,49 +11504,6 @@ "node": ">=10" } }, - "node_modules/@wessberg/ts-evaluator/node_modules/domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wessberg/ts-evaluator/node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, "node_modules/@wessberg/ts-evaluator/node_modules/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -12751,16 +11633,6 @@ "node": ">=10" } }, - "node_modules/@wessberg/ts-evaluator/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@wessberg/ts-evaluator/node_modules/tr46": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", @@ -12773,6 +11645,21 @@ "node": ">=8" } }, + "node_modules/@wessberg/ts-evaluator/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/@wessberg/ts-evaluator/node_modules/w3c-xmlserializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", @@ -12823,12 +11710,42 @@ "node": ">=10" } }, + "node_modules/@wessberg/ts-evaluator/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@wessberg/ts-evaluator/node_modules/xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -12909,6 +11826,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", "dev": true }, "node_modules/abbrev": { @@ -12952,20 +11870,23 @@ } }, "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", "dev": true, "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" } }, - "node_modules/acorn-globals/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { "node": ">=0.4.0" } @@ -12988,29 +11909,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-node/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", @@ -13068,13 +11966,11 @@ } }, "node_modules/agentkeepalive": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "dev": true, "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", "humanize-ms": "^1.2.1" }, "engines": { @@ -13139,16 +12035,6 @@ "ajv": "^8.8.2" } }, - "node_modules/amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.4.2" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -13361,26 +12247,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/app-builder-lib/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/builder-util-runtime": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", - "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=12" } }, "node_modules/app-builder-lib/node_modules/js-yaml": { @@ -13476,6 +12354,17 @@ "node": ">= 6" } }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/archiver-utils/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -13497,19 +12386,50 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/archiver/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "peer": true + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "peer": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 6" + "node": "*" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" } }, "node_modules/archy": { @@ -13530,19 +12450,6 @@ "node": ">=10" } }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -13578,9 +12485,9 @@ } }, "node_modules/aria-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", - "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", "dev": true, "dependencies": { "tslib": "^2.0.0" @@ -13590,12 +12497,12 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/arr-diff": { @@ -13650,13 +12557,16 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13689,22 +12599,17 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, - "node_modules/array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==", - "dev": true - }, "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -13799,16 +12704,17 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -13854,17 +12760,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -13906,37 +12813,16 @@ "node": ">=0.10.0" } }, - "node_modules/ast-transform": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz", - "integrity": "sha512-e/JfLiSoakfmL4wmTGPjv0HpTICVmxwXgYOB8x+mzozHL8v+dSfCbrJ8J8hJ0YBP0XcYu1aLZ6b/3TnxNK3P2A==", + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", "dev": true, "dependencies": { - "escodegen": "~1.2.0", - "esprima": "~1.0.4", - "through": "~2.3.4" - } - }, - "node_modules/ast-transform/node_modules/esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "tslib": "^2.0.1" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ast-types": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz", - "integrity": "sha512-RIOpVnVlltB6PcBJ5BMLx+H+6JJ/zjDGU0t7f0L6c2M1dqcK92VQopLBlPQ9R80AVXelfqYgjcPLtHtDbNFg0Q==", - "dev": true, - "engines": { - "node": ">= 0.6" + "node": ">=4" } }, "node_modules/astral-regex": { @@ -13950,9 +12836,9 @@ } }, "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, "node_modules/async-done": { @@ -14081,10 +12967,13 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -14093,21 +12982,21 @@ } }, "node_modules/axe-core": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz", - "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==", + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.4.tgz", + "integrity": "sha512-CZLSKisu/bhJ2awW4kJndluz2HLZYIHh5Uy1+ZwDRkJi69811xgIXXfdU9HSLX0Th+ILrHj8qfL/5wzamsFtQg==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -14131,16 +13020,16 @@ } }, "node_modules/babel-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz", - "integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "peer": true, "dependencies": { - "@jest/transform": "^29.5.0", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.5.0", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -14299,9 +13188,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", - "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, "peer": true, "dependencies": { @@ -14315,13 +13204,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", - "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", + "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.5.0", + "@babel/helper-define-polyfill-provider": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { @@ -14378,6 +13267,22 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -14403,13 +13308,13 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", - "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, "peer": true, "dependencies": { - "babel-plugin-jest-hoist": "^29.5.0", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -14454,6 +13359,13 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, "node_modules/base": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", @@ -14574,12 +13486,15 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/binaryextensions": { @@ -14637,19 +13552,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -14666,13 +13568,13 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -14680,7 +13582,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -14707,22 +13609,6 @@ "ms": "2.0.0" } }, - "node_modules/body-parser/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -14757,23 +13643,15 @@ } }, "node_modules/bonjour-service": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", - "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, - "node_modules/bonjour-service/node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -14827,12 +13705,12 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -14885,31 +13763,6 @@ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.3.tgz", "integrity": "sha512-Og0+jCRQetV84U8wVjMNccfGCnMQ9mGs9Hv78QFe+pSDD3gWTpz0y+1QCuxy5d/vBFuZ3iwP2eycAkvqIMPmWg==" }, - "node_modules/brfs": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brfs/-/brfs-2.0.2.tgz", - "integrity": "sha512-IrFjVtwu4eTJZyu8w/V2gxU7iLTtcHih67sgEdzrhjLBMHp2uYefUBfdM4k2UvcuWMgV7PQDZHSLeNWnLFKWVQ==", - "dev": true, - "dependencies": { - "quote-stream": "^1.0.1", - "resolve": "^1.1.5", - "static-module": "^3.0.2", - "through2": "^2.0.0" - }, - "bin": { - "brfs": "bin/cmd.js" - } - }, - "node_modules/brfs/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/brotli": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", @@ -14936,32 +13789,6 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, - "node_modules/browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "dependencies": { - "resolve": "1.1.7" - } - }, - "node_modules/browser-resolve/node_modules/resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", - "dev": true - }, - "node_modules/browserify-optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz", - "integrity": "sha512-VrhjbZ+Ba5mDiSYEuPelekQMfTbhcA2DhLk2VQWqdcCROWeFqlTcXZ7yfRkXCIl8E+g4gINJYJiRB7WEtfomAQ==", - "dev": true, - "dependencies": { - "ast-transform": "0.0.0", - "ast-types": "^0.7.0", - "browser-resolve": "^1.8.1" - } - }, "node_modules/browserify-zlib": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", @@ -15056,12 +13883,15 @@ } }, "node_modules/buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", "dev": true, "engines": { - "node": ">=0.4.0" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/buffer-from": { @@ -15106,9 +13936,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", - "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -15124,17 +13954,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/builder-util/node_modules/builder-util-runtime": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", - "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=12" } }, "node_modules/builder-util/node_modules/https-proxy-agent": { @@ -15171,6 +14002,21 @@ "semver": "^7.0.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -15198,54 +14044,48 @@ "dev": true }, "node_modules/cacache": { - "version": "17.1.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", - "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", "dev": true, "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", - "minipass": "^7.0.3", + "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", "p-map": "^4.0.0", - "ssri": "^10.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", "tar": "^6.1.11", - "unique-filename": "^3.0.0" + "unique-filename": "^2.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/cacache/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -15261,29 +14101,92 @@ } }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/cacache/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, + "node_modules/cacache/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -15326,9 +14229,9 @@ } }, "node_modules/cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "dependencies": { "clone-response": "^1.0.2", @@ -15359,13 +14262,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15418,9 +14326,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001591", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", - "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", + "version": "1.0.30001600", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", + "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", "funding": [ { "type": "opencollective", @@ -15621,9 +14529,9 @@ "dev": true }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "funding": [ { "type": "github", @@ -15634,6 +14542,15 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -15667,78 +14584,23 @@ "node": ">=0.10.0" } }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/clean-css": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", - "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", "dev": true, "dependencies": { "source-map": "~0.6.0" @@ -15777,9 +14639,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", - "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "engines": { "node": ">=6" }, @@ -15788,9 +14650,9 @@ } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", + "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", "dev": true, "dependencies": { "string-width": "^4.2.0" @@ -15841,6 +14703,23 @@ "node": ">=12" } }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", @@ -15922,6 +14801,36 @@ "readable-stream": "^2.3.5" } }, + "node_modules/cloneable-readable/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cloneable-readable/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -15958,9 +14867,9 @@ } }, "node_modules/collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true, "peer": true }, @@ -16080,10 +14989,13 @@ } }, "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/compress-commons": { "version": "4.1.2", @@ -16101,21 +15013,6 @@ "node": ">= 10" } }, - "node_modules/compress-commons/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -16180,6 +15077,33 @@ "typedarray": "^0.0.6" } }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/concurrently": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", @@ -16222,24 +15146,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/concurrently/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/conf": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", @@ -16290,65 +15196,10 @@ "typescript": "^5.3.3" } }, - "node_modules/config-file-ts/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/config-file-ts/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/config-file-ts/node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -16397,6 +15248,15 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -16453,9 +15313,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" @@ -16516,20 +15376,20 @@ "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==" }, "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, "dependencies": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -16539,6 +15399,34 @@ "webpack": "^5.1.0" } }, + "node_modules/copy-webpack-plugin/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/copy-webpack-plugin/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -16552,28 +15440,29 @@ } }, "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz", - "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "node_modules/copy-webpack-plugin/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, "engines": { "node": ">=12" @@ -16582,10 +15471,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/core-js": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.34.0.tgz", - "integrity": "sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==", + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz", + "integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -16593,12 +15494,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", - "integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz", + "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==", "dev": true, "dependencies": { - "browserslist": "^4.22.3" + "browserslist": "^4.23.0" }, "funding": { "type": "opencollective", @@ -16606,9 +15507,9 @@ } }, "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -16624,19 +15525,47 @@ } }, "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/crc": { @@ -16701,19 +15630,26 @@ "node": ">= 10" } }, - "node_modules/crc32-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, "peer": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" }, "engines": { - "node": ">= 6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/credit-card-type": { @@ -16782,19 +15718,19 @@ } }, "node_modules/css-loader": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", - "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.21", + "postcss": "^8.4.33", "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.3", - "postcss-modules-scope": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">= 12.13.0" @@ -16804,7 +15740,16 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/css-select": { @@ -16854,9 +15799,9 @@ } }, "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, "node_modules/cssstyle": { @@ -16871,27 +15816,24 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", "dev": true, "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" } }, - "node_modules/dash-ast": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-2.0.1.tgz", - "integrity": "sha512-5TXltWJGc+RdnabUGzhRae1TRq6m4gr+3K2wQX0is5/F2yS6MJXJvLyI3ErAnsAXuJoGqvfVD5icRgim07DrxQ==", - "dev": true - }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -16904,12 +15846,55 @@ "node": ">=18" } }, - "node_modules/data-urls/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/date-fns": { @@ -17035,40 +16020,24 @@ } }, "node_modules/dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", "dev": true, - "peer": true + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } }, "node_modules/deep-equal": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.1.tgz", - "integrity": "sha512-lKdkdV6EOGoVn65XaOsPdH4rMxTZOnmFyuIkMjM1i5HHCbfjC97dawgTAy0deYNfuqUqW+Q5VrVaQYtUpSd6yQ==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.0", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==" }, "node_modules/deep-extend": { "version": "0.6.0", @@ -17094,6 +16063,22 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-browser-id": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", @@ -17110,6 +16095,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-browser/node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -17164,16 +16161,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -17185,11 +16185,12 @@ } }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -17214,9 +16215,9 @@ } }, "node_modules/defu": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.2.tgz", - "integrity": "sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "dev": true }, "node_modules/del": { @@ -17241,6 +16242,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/del/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/del/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -17261,6 +16272,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/del/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/del/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -17334,9 +16357,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { "node": ">=8" } @@ -17401,10 +16424,19 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -17420,6 +16452,28 @@ "minimatch": "^3.0.4" } }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -17461,17 +16515,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/dmg-builder/node_modules/builder-util-runtime": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", - "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=12" } }, "node_modules/dmg-builder/node_modules/js-yaml": { @@ -17536,16 +16591,10 @@ "dev": true, "optional": true }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, "node_modules/dns-packet": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", - "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" @@ -17608,15 +16657,25 @@ ] }, "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "dependencies": { - "webidl-conversions": "^7.0.0" + "webidl-conversions": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/domhandler": { @@ -17680,15 +16739,15 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -17711,15 +16770,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -17732,6 +16782,36 @@ "stream-shift": "^1.0.0" } }, + "node_modules/duplexify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/each-props": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", @@ -17799,15 +16879,6 @@ "node": ">=14" } }, - "node_modules/editorconfig/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/editorconfig/node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -17853,9 +16924,9 @@ } }, "node_modules/electron": { - "version": "28.2.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-28.2.8.tgz", - "integrity": "sha512-VgXw2OHqPJkobIC7X9eWh3atptjnELaP+zlbF9Oz00ridlaOWmtLPsp6OaXbLw35URpMr0iYesq8okKp7S0k+g==", + "version": "28.3.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-28.3.1.tgz", + "integrity": "sha512-aF9fONuhVDJlctJS7YOw76ynxVAQdfIWmlhRMKits24tDcdSL0eMHUS0wWYiRfGWbQnUKB6V49Rf17o32f4/fg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -17909,17 +16980,33 @@ "fs-extra": "^10.1.0" } }, - "node_modules/electron-builder/node_modules/builder-util-runtime": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", - "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, + "peer": true, "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/electron-log": { @@ -17946,17 +17033,18 @@ "mime": "^2.5.2" } }, - "node_modules/electron-publish/node_modules/builder-util-runtime": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", - "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=12" } }, "node_modules/electron-reload": { @@ -17982,9 +17070,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.676", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.676.tgz", - "integrity": "sha512-uHt4FB8SeYdhcOsj2ix/C39S7sPSNFJpzShjxGOm1KdF4MHyGqGi389+T5cErsodsijojXilYaHIKKqJfqh7uQ==" + "version": "1.4.715", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz", + "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" }, "node_modules/electron-updater": { "version": "6.1.8", @@ -18008,6 +17096,33 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz", + "integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/electron-updater/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -18083,9 +17198,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -18116,9 +17231,9 @@ } }, "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.1.tgz", + "integrity": "sha512-8PiZgZNIB4q/Lw4AhOvAfB/ityHAd2bli3lESSWmWSzSsl5dKpy5N1d1Rfkd2teq/g9xN90lc6o98DOjMeYHpg==", "dev": true, "bin": { "envinfo": "dist/cli.js" @@ -18156,50 +17271,57 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", + "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -18208,6 +17330,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -18229,32 +17370,44 @@ } }, "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.2.tgz", + "integrity": "sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw==", "dev": true }, - "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -18275,14 +17428,15 @@ } }, "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", "dev": true, "hasInstallScript": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", "next-tick": "^1.1.0" }, "engines": { @@ -18307,43 +17461,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha512-mz3UqCh0uPCIqsw1SSAkB/p0rOzF/M0V++vyN7JqlPtSW/VsYgQBvVvqMLmfBuyMzTpLnNqi6JmcSizs4jy19A==", - "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "node_modules/es6-set": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.6.tgz", - "integrity": "sha512-TE3LgGLDIBX332jq3ypv6bcOpkLO0AslAQo7p2VqX/1N46YNsvIWgvjojjSEnWEGWMhr1qUbYeTSir5J6mFHOw==", - "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "es6-iterator": "~2.0.3", - "es6-symbol": "^3.1.3", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/es6-set/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "dev": true - }, "node_modules/es6-shim": { "version": "0.35.8", "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", @@ -18351,13 +17468,16 @@ "dev": true }, "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", "dev": true, "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" } }, "node_modules/es6-weak-map": { @@ -18440,9 +17560,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "engines": { "node": ">=6" } @@ -18465,68 +17585,34 @@ } }, "node_modules/escodegen": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz", - "integrity": "sha512-yLy3Cc+zAC0WSmoT2fig3J87TpQ8UaZGx8ahCAs9FL8qNbyV7CVyPKS74DG4bsHiL5ew9sxdYx131OkBQMFnvA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "dependencies": { - "esprima": "~1.0.4", - "estraverse": "~1.5.0", - "esutils": "~1.0.0" + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" }, "bin": { "escodegen": "bin/escodegen.js", "esgenerate": "bin/esgenerate.js" }, "engines": { - "node": ">=0.4.0" + "node": ">=6.0" }, "optionalDependencies": { - "source-map": "~0.1.30" - } - }, - "node_modules/escodegen/node_modules/esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", - "integrity": "sha512-FpCjJDfmo3vsc/1zKSeqR5k42tcIhxFIlvq+h9j0fO2q/h2uLKyweq7rYJ+0CoVvrGQOxIS5wyBrW/+vF58BUQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/escodegen/node_modules/esutils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", - "integrity": "sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "source-map": "~0.6.1" } }, "node_modules/escodegen/node_modules/source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "dependencies": { - "amdefine": ">=0.0.4" - }, "engines": { - "node": ">=0.8.0" + "node": ">=0.10.0" } }, "node_modules/eslint": { @@ -18674,9 +17760,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -18730,6 +17816,16 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -18751,6 +17847,18 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -18891,6 +17999,16 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -18936,6 +18054,18 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -18948,6 +18078,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -19011,12 +18156,6 @@ "node": ">=4.0" } }, - "node_modules/estree-is-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-is-function/-/estree-is-function-1.0.0.tgz", - "integrity": "sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA==", - "dev": true - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -19084,9 +18223,9 @@ "dev": true }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, "node_modules/events": { @@ -19190,72 +18329,17 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/expand-brackets/node_modules/is-extendable": { @@ -19295,33 +18379,39 @@ } }, "node_modules/expect": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", - "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -19379,20 +18469,16 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/express/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">= 0.8" + "node": ">=4" } }, "node_modules/express/node_modules/ms": { @@ -19442,6 +18528,36 @@ } ] }, + "node_modules/express/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -19451,12 +18567,6 @@ "type": "^2.7.2" } }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "dev": true - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -19622,9 +18732,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -19659,9 +18769,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -19786,15 +18896,6 @@ "minimatch": "^5.0.1" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -20185,18 +19286,29 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/flat-cache/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -20217,6 +19329,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/flat-cache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/flat-cache/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -20233,15 +19357,15 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/flow-parser": { - "version": "0.223.3", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.223.3.tgz", - "integrity": "sha512-9KxxDKSB22ovMpSULbOL/QAQGPN6M0YMS3PubQvB0jVc4W7QP6VhasIVic7MzKcJSh0BAVs4J6SZjoH0lDDNlg==", + "version": "0.231.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.231.0.tgz", + "integrity": "sha512-WVzuqwq7ZnvBceCG0DGeTQebZE+iIU0mlk5PmJgYj9DDrt+0isGC2m1ezW9vxL4V+HERJJo9ExppOnwKH2op6Q==", "dev": true, "engines": { "node": ">=0.4.0" @@ -20257,10 +19381,40 @@ "readable-stream": "^2.3.6" } }, + "node_modules/flush-write-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/flush-write-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/flush-write-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -20324,9 +19478,9 @@ } }, "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "engines": { "node": ">=14" @@ -20394,12 +19548,64 @@ "ajv": "^6.9.1" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -20418,6 +19624,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -20497,6 +19712,36 @@ "readable-stream": "^2.0.0" } }, + "node_modules/from2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -20504,9 +19749,9 @@ "dev": true }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -20514,30 +19759,36 @@ "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dependencies": { - "minipass": "^7.0.3" + "minipass": "^3.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 8" } }, "node_modules/fs-minipass/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" } }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -20551,6 +19802,36 @@ "node": ">= 0.10" } }, + "node_modules/fs-mkdirp-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/fs-mkdirp-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/fs-mkdirp-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/fs-mkdirp-stream/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -20562,9 +19843,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", "dev": true }, "node_modules/fs.realpath": { @@ -20573,9 +19854,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -20648,12 +19929,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", - "dev": true - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -20676,15 +19951,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -20699,9 +19978,9 @@ } }, "node_modules/get-npm-tarball-url": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.0.3.tgz", - "integrity": "sha512-R/PW6RqyaBQNWYaSyfrh54/qtcnOp22FHCCiRhSSZj0FP3KQWCsxxt0DzIdVTbwTqe9CtQfvl/FPD4UIPt4pqw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz", + "integrity": "sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA==", "dev": true, "engines": { "node": ">=12.17" @@ -20741,13 +20020,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -20757,10 +20037,13 @@ } }, "node_modules/get-tsconfig": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.5.0.tgz", - "integrity": "sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", + "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, "funding": { "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } @@ -20775,36 +20058,24 @@ } }, "node_modules/giget": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.1.2.tgz", - "integrity": "sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", "dev": true, "dependencies": { - "colorette": "^2.0.19", - "defu": "^6.1.2", - "https-proxy-agent": "^5.0.1", - "mri": "^1.2.0", - "node-fetch-native": "^1.0.2", - "pathe": "^1.1.0", - "tar": "^6.1.13" + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" }, "bin": { "giget": "dist/cli.mjs" } }, - "node_modules/giget/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -20818,19 +20089,22 @@ "dev": true }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -20869,6 +20143,16 @@ "node": ">= 0.10" } }, + "node_modules/glob-stream/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/glob-stream/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -20911,6 +20195,61 @@ "node": ">=0.10.0" } }, + "node_modules/glob-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/glob-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/glob-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/glob-stream/node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -21129,6 +20468,12 @@ "node": ">=0.10.0" } }, + "node_modules/glob-watcher/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/glob-watcher/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -21174,6 +20519,21 @@ "node": ">=0.10.0" } }, + "node_modules/glob-watcher/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/glob-watcher/node_modules/readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -21188,6 +20548,15 @@ "node": ">=0.10" } }, + "node_modules/glob-watcher/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/glob-watcher/node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -21201,27 +20570,6 @@ "node": ">=0.10.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -21629,9 +20977,9 @@ } }, "node_modules/gulp-cli/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -21752,19 +21100,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gulp-filter/node_modules/to-absolute-glob": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-3.0.0.tgz", - "integrity": "sha512-loO/XEWTRqpfcpI7+Jr2RR2Umaaozx1t6OSVWtMi0oy5F/Fxg3IC+D/TToDnxyAGs7uZBGT/6XmyDUxgsObJXA==", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gulp-if": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", @@ -21789,20 +21124,6 @@ "through2": "^4.0.2" } }, - "node_modules/gulp-json-editor/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/gulp-json-editor/node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -21821,6 +21142,28 @@ "minimatch": "^3.0.3" } }, + "node_modules/gulp-match/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/gulp-match/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/gulp-plugin-extras": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/gulp-plugin-extras/-/gulp-plugin-extras-0.3.0.tgz", @@ -21892,15 +21235,6 @@ } } }, - "node_modules/gulp-zip/node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/gulp-zip/node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -21913,31 +21247,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gulp-zip/node_modules/replace-ext": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", - "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/gulp-zip/node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", - "dev": true, - "dependencies": { - "clone": "^2.1.2", - "clone-stats": "^1.0.0", - "remove-trailing-separator": "^1.1.0", - "replace-ext": "^2.0.0", - "teex": "^1.0.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/gulplog": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", @@ -21967,6 +21276,36 @@ "gunzip-maybe": "bin.js" } }, + "node_modules/gunzip-maybe/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/gunzip-maybe/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/gunzip-maybe/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/gunzip-maybe/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -22023,13 +21362,10 @@ } }, "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, "engines": { "node": ">= 0.4.0" } @@ -22052,20 +21388,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -22085,11 +21421,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -22173,9 +21509,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -22269,22 +21605,51 @@ "wbuf": "^1.1.0" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { - "whatwg-encoding": "^2.0.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/html-entities": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", - "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "dev": true, "funding": [ { @@ -22305,16 +21670,16 @@ "peer": true }, "node_modules/html-loader": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-4.2.0.tgz", - "integrity": "sha512-OxCHD3yt+qwqng2vvcaPApCEvbx+nXWu+v69TYHx1FO8bffHn/JjHtE3TTQZmHjwvnJe4xxzuecetDVBrQR1Zg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-5.0.0.tgz", + "integrity": "sha512-puaGKdjdVVIFRtgIC2n5dt5bt0N5j6heXlAQZ4Do1MLjHmOT1gCE1Ogg7XZNeJlnOVHHsrZKGs5dfh+XwZ3XPw==", "dev": true, "dependencies": { - "html-minifier-terser": "^7.0.0", - "parse5": "^7.0.0" + "html-minifier-terser": "^7.2.0", + "parse5": "^7.1.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -22361,9 +21726,9 @@ "dev": true }, "node_modules/html-webpack-plugin": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.4.tgz", - "integrity": "sha512-3wNSaVVxdxcu0jd4FpQFoICdqgxs4zIQQvj+2yQKFfBOnLETQ6X5CDWdeasuGlSsooFlMkEioWDTqBv1wvw5Iw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -22380,7 +21745,16 @@ "url": "https://opencollective.com/html-webpack-plugin" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/html-webpack-plugin/node_modules/commander": { @@ -22444,10 +21818,36 @@ "node": ">= 0.8" } }, - "node_modules/http-assert/node_modules/deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==" + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } }, "node_modules/http-auth": { "version": "4.1.9", @@ -22473,6 +21873,15 @@ "node": ">=8" } }, + "node_modules/http-auth/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -22486,34 +21895,18 @@ "dev": true }, "node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dependencies": { - "depd": "~1.1.2", + "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", + "statuses": "2.0.1", "toidentifier": "1.0.1" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/http-parser-js": { @@ -22586,6 +21979,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -22656,9 +22055,9 @@ } }, "node_modules/i18next": { - "version": "23.7.7", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.7.7.tgz", - "integrity": "sha512-peTvdT+Lma+o0LfLFD7IC2M37N9DJ04dH0IJYOyOHRhDfLo6nK36v7LkrQH35C2l8NHiiXZqGirhKESlEb/5PA==", + "version": "23.10.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.1.tgz", + "integrity": "sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==", "dev": true, "funding": [ { @@ -22679,9 +22078,9 @@ } }, "node_modules/i18next/node_modules/@babel/runtime": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", - "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -22691,9 +22090,9 @@ } }, "node_modules/i18next/node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, "node_modules/iconv-corefoundation": { @@ -22769,9 +22168,9 @@ "dev": true }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -22789,30 +22188,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -22832,9 +22207,9 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, "node_modules/import-fresh": { @@ -22970,9 +22345,9 @@ "dev": true }, "node_modules/inflation": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", - "integrity": "sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", "engines": { "node": ">= 0.8.0" } @@ -23030,27 +22405,14 @@ "node": ">=12.0.0" } }, - "node_modules/inquirer/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -23106,6 +22468,25 @@ "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", "dev": true }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -23138,24 +22519,15 @@ } }, "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/is-arguments": { @@ -23175,14 +22547,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -23294,24 +22668,30 @@ } }, "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "hasown": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-date-object": { @@ -23336,26 +22716,16 @@ "dev": true }, "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/is-docker": { @@ -23458,6 +22828,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -23473,10 +22876,13 @@ "dev": true }, "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -23507,9 +22913,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -23518,6 +22924,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -23628,21 +23046,27 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -23691,12 +23115,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -23744,10 +23168,13 @@ } }, "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -23765,13 +23192,16 @@ } }, "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -23809,12 +23239,12 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, "node_modules/isbinaryfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz", - "integrity": "sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz", + "integrity": "sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==", "dev": true, "engines": { - "node": ">= 14.0.0" + "node": ">= 18.0.0" }, "funding": { "url": "https://github.com/sponsors/gjtorikian/" @@ -23835,9 +23265,9 @@ } }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" @@ -23860,27 +23290,43 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "peer": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/istanbul-lib-source-maps": { @@ -23909,9 +23355,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", - "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "peer": true, "dependencies": { @@ -23957,9 +23403,9 @@ } }, "node_modules/jake": { - "version": "10.8.6", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.6.tgz", - "integrity": "sha512-G43Ub9IYEFfu72sua6rzooi8V8Gz2lkfk48rW20vEWCGizeaEPlKB1Kh8JIA84yQbiAEfqlPmSpGgCKKxH3rDA==", + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", "dev": true, "dependencies": { "async": "^3.2.3", @@ -23974,17 +23420,39 @@ "node": ">=10" } }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.5.0.tgz", - "integrity": "sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "peer": true, "dependencies": { - "@jest/core": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^29.5.0" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" @@ -24002,13 +23470,14 @@ } }, "node_modules/jest-changed-files": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", - "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "peer": true, "dependencies": { "execa": "^5.0.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0" }, "engines": { @@ -24016,29 +23485,29 @@ } }, "node_modules/jest-circus": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.5.0.tgz", - "integrity": "sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, "peer": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/expect": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "dedent": "^0.7.0", + "dedent": "^1.0.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.5.0", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "p-limit": "^3.1.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" @@ -24047,24 +23516,58 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-cli": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.5.0.tgz", - "integrity": "sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==", + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "peer": true, "dependencies": { - "@jest/core": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", - "prompts": "^2.0.1", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "yargs": "^17.3.1" }, "bin": { @@ -24083,32 +23586,32 @@ } }, "node_modules/jest-config": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.5.0.tgz", - "integrity": "sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "peer": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.5.0", - "@jest/types": "^29.5.0", - "babel-jest": "^29.5.0", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.5.0", - "jest-environment-node": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-runner": "^29.5.0", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -24128,6 +23631,30 @@ } } }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/jest-config/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -24149,25 +23676,92 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-diff": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", - "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "peer": true, "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.4.3", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/jest-docblock": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", - "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "peer": true, "dependencies": { @@ -24178,35 +23772,70 @@ } }, "node_modules/jest-each": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.5.0.tgz", - "integrity": "sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "peer": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", - "jest-util": "^29.5.0", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, "node_modules/jest-environment-jsdom": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.5.0.tgz", - "integrity": "sha512-/KG8yEK4aN8ak56yFVdqFDzKNHgF4BAymCx2LbPNPsUshUlfAl0eX402Xm1pt+eoG9SLZEUVifqXtX8SK74KCw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/jsdom": "^20.0.0", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", "jsdom": "^20.0.0" }, "engines": { @@ -24232,6 +23861,31 @@ "parse5": "^7.0.0" } }, + "node_modules/jest-environment-jsdom/node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/jest-environment-jsdom/node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, "node_modules/jest-environment-jsdom/node_modules/cssstyle": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", @@ -24264,26 +23918,29 @@ "node": ">=12" } }, - "node_modules/jest-environment-jsdom/node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "node_modules/jest-environment-jsdom/node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=6.0" + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" }, - "optionalDependencies": { - "source-map": "~0.6.1" + "engines": { + "node": ">=12" } }, "node_modules/jest-environment-jsdom/node_modules/https-proxy-agent": { @@ -24299,6 +23956,20 @@ "node": ">= 6" } }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/jest-environment-jsdom/node_modules/jsdom": { "version": "20.0.3", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", @@ -24344,55 +24015,6 @@ } } }, - "node_modules/jest-environment-jsdom/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/jest-environment-jsdom/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/jest-environment-jsdom/node_modules/tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -24405,16 +24027,37 @@ "node": ">=12" } }, - "node_modules/jest-environment-jsdom/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", "dev": true, "dependencies": { - "prelude-ls": "~1.1.2" + "xml-name-validator": "^4.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" } }, "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { @@ -24430,69 +24073,72 @@ "node": ">=12" } }, - "node_modules/jest-environment-jsdom/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node": ">=12" } }, "node_modules/jest-environment-node": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.5.0.tgz", - "integrity": "sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "peer": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^29.5.0", - "jest-util": "^29.5.0" + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", - "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz", - "integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.4.3", - "jest-util": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -24518,48 +24164,124 @@ "node": ">=10.12.0" } }, + "node_modules/jest-junit/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest-leak-detector": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz", - "integrity": "sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "peer": true, "dependencies": { - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, "node_modules/jest-matcher-utils": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", - "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "pretty-format": "^29.5.0" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/jest-message-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", - "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.5.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -24567,20 +24289,51 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-mock": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.5.0.tgz", - "integrity": "sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==", + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", - "@types/node": "*", - "jest-util": "^29.5.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dev": true, + "dependencies": { + "@jest/types": "^27.5.1", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/jest-mock-extended": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.5.tgz", @@ -24594,6 +24347,31 @@ "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-mock/node_modules/@types/yargs": { + "version": "16.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.9.tgz", + "integrity": "sha512-tHhzvkFXZQeTECenFoRljLBYPZJ7jAVxqqtEI0qTLOmuultnFp4I9yKE17vTuhf7BkhCu7I4XuemPgikDVuYqA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -24640,28 +24418,60 @@ "typescript": ">=4.8" } }, + "node_modules/jest-preset-angular/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-preset-angular/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-preset-angular/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/jest-regex-util": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", - "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.5.0.tgz", - "integrity": "sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "peer": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.5.0", - "jest-validate": "^29.5.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" @@ -24671,45 +24481,45 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz", - "integrity": "sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "peer": true, "dependencies": { - "jest-regex-util": "^29.4.3", - "jest-snapshot": "^29.5.0" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.5.0.tgz", - "integrity": "sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "peer": true, "dependencies": { - "@jest/console": "^29.5.0", - "@jest/environment": "^29.5.0", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.4.3", - "jest-environment-node": "^29.5.0", - "jest-haste-map": "^29.5.0", - "jest-leak-detector": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-resolve": "^29.5.0", - "jest-runtime": "^29.5.0", - "jest-util": "^29.5.0", - "jest-watcher": "^29.5.0", - "jest-worker": "^29.5.0", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -24739,32 +24549,32 @@ } }, "node_modules/jest-runtime": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.5.0.tgz", - "integrity": "sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, "peer": true, "dependencies": { - "@jest/environment": "^29.5.0", - "@jest/fake-timers": "^29.5.0", - "@jest/globals": "^29.5.0", - "@jest/source-map": "^29.4.3", - "@jest/test-result": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-mock": "^29.5.0", - "jest-regex-util": "^29.4.3", - "jest-resolve": "^29.5.0", - "jest-snapshot": "^29.5.0", - "jest-util": "^29.5.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -24772,6 +24582,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/jest-runtime/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -24793,10 +24614,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jest-runtime/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest-snapshot": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.5.0.tgz", - "integrity": "sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "peer": true, "dependencies": { @@ -24804,37 +24653,69 @@ "@babel/generator": "^7.7.2", "@babel/plugin-syntax-jsx": "^7.7.2", "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.5.0", - "@jest/transform": "^29.5.0", - "@jest/types": "^29.5.0", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.5.0", + "expect": "^29.7.0", "graceful-fs": "^4.2.9", - "jest-diff": "^29.5.0", - "jest-get-type": "^29.4.3", - "jest-matcher-utils": "^29.5.0", - "jest-message-util": "^29.5.0", - "jest-util": "^29.5.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^29.5.0", - "semver": "^7.3.5" + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, "node_modules/jest-util": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", - "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -24846,23 +24727,36 @@ } }, "node_modules/jest-validate": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.5.0.tgz", - "integrity": "sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "peer": true, "dependencies": { - "@jest/types": "^29.5.0", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.4.3", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.5.0" + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/jest-validate/node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -24876,20 +24770,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-watcher": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.5.0.tgz", - "integrity": "sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==", + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "peer": true, "dependencies": { - "@jest/test-result": "^29.5.0", - "@jest/types": "^29.5.0", + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true, + "peer": true + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { @@ -24897,13 +24813,13 @@ } }, "node_modules/jest-worker": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz", - "integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.5.0", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -24936,14 +24852,14 @@ } }, "node_modules/joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "dev": true, "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } @@ -24954,14 +24870,15 @@ "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "node_modules/js-beautify": { - "version": "1.14.11", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz", - "integrity": "sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", "dev": true, "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^1.0.3", + "editorconfig": "^1.0.4", "glob": "^10.3.3", + "js-cookie": "^3.0.5", "nopt": "^7.2.0" }, "bin": { @@ -24982,61 +24899,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/js-beautify/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/js-beautify/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/js-beautify/node_modules/nopt": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", @@ -25052,6 +24914,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/js-message": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", @@ -25091,10 +24962,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jscodeshift": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.1.tgz", - "integrity": "sha512-hIJfxUy8Rt4HkJn/zZPU9ChKfKZM1342waJ1QC2e2YsPcWhM+3BJ4dcfQCzArTrk1jJeNLB341H+qOcEHRxJZg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", + "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", "dev": true, "dependencies": { "@babel/core": "^7.23.0", @@ -25191,21 +25068,10 @@ "node": ">= 14" } }, - "node_modules/jsdom/node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -25214,64 +25080,6 @@ "node": ">= 14" } }, - "node_modules/jsdom/node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "engines": { - "node": ">=18" - } - }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "engines": { - "node": ">=18" - } - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -25308,9 +25116,9 @@ "dev": true }, "node_modules/json-stable-stringify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", - "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", + "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", "dependencies": { "call-bind": "^1.0.5", "isarray": "^2.0.5", @@ -25396,11 +25204,38 @@ "setimmediate": "^1.0.5" } }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/jszip/node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/just-debounce": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.1.0.tgz", @@ -25439,9 +25274,9 @@ } }, "node_modules/keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -25559,6 +25394,29 @@ "streaming-json-stringify": "3" } }, + "node_modules/koa/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/koa/node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -25581,13 +25439,13 @@ } }, "node_modules/launch-editor": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", - "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", "dev": true, "dependencies": { "picocolors": "^1.0.0", - "shell-quote": "^1.7.3" + "shell-quote": "^1.8.1" } }, "node_modules/lazy-universal-dotenv": { @@ -25622,6 +25480,36 @@ "node": ">= 0.6.3" } }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -25730,9 +25618,9 @@ } }, "node_modules/less/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "optional": true, "bin": { @@ -25828,12 +25716,12 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", "dev": true, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/lines-and-columns": { @@ -25937,15 +25825,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", - "dev": true, - "engines": { - "node": ">=14" - } - }, "node_modules/lint-staged/node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -25959,9 +25838,9 @@ } }, "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -26024,15 +25903,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lint-staged/node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/listr2": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", @@ -26096,12 +25966,6 @@ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, - "node_modules/listr2/node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, "node_modules/listr2/node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -26131,9 +25995,9 @@ } }, "node_modules/listr2/node_modules/string-width": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.0.0.tgz", - "integrity": "sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", "dev": true, "dependencies": { "emoji-regex": "^10.3.0", @@ -26397,13 +26261,10 @@ } }, "node_modules/log-update/node_modules/ansi-escapes": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", - "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", "dev": true, - "dependencies": { - "type-fest": "^3.0.0" - }, "engines": { "node": ">=14.16" }, @@ -26504,9 +26365,9 @@ } }, "node_modules/log-update/node_modules/string-width": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.0.0.tgz", - "integrity": "sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", "dev": true, "dependencies": { "emoji-regex": "^10.3.0", @@ -26535,18 +26396,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -26565,9 +26414,9 @@ } }, "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", "dev": true, "engines": { "node": ">= 0.6.0" @@ -26699,9 +26548,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -26739,60 +26588,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -26827,77 +26622,6 @@ "node": ">=8" } }, - "node_modules/make-fetch-happen/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/make-fetch-happen/node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/make-fetch-happen/node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/make-fetch-happen/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -26978,9 +26702,9 @@ } }, "node_modules/markdown-to-jsx": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.1.tgz", - "integrity": "sha512-GbrbkTnHp9u6+HqbPRFJbObi369AgJNXi/sGqq5HRsoZW063xR1XDCaConqq+whfEIAlzB1YPnOgsPc7B7bc/A==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.5.tgz", + "integrity": "sha512-c8NB0H/ig+FOWssE9be0PKsYbCDhcWEkicxMnpdfUuHbFljnen4LAdgUShOyR/PgO3/qKvt9cwfQ0U/zQvZ44A==", "dev": true, "engines": { "node": ">= 10" @@ -27243,9 +26967,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz", - "integrity": "sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", "dev": true, "dependencies": { "@types/mdast": "^3.0.0", @@ -27431,12 +27155,12 @@ } }, "node_modules/memfs": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.1.tgz", - "integrity": "sha512-UWbFJKvj5k+nETdteFndTpYxdeTMox/ULeqX5k/dpaQJCCFmj5EeKv3dBcyO2xmkRAx2vppRu5dVG7SOtsGOzA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, "dependencies": { - "fs-monkey": "^1.0.3" + "fs-monkey": "^1.0.4" }, "engines": { "node": ">= 4.0.0" @@ -27457,24 +27181,6 @@ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "dev": true }, - "node_modules/merge-source-map": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", - "integrity": "sha512-PGSmS0kfnTnMJCzJ16BLLCEe6oeYCamKFFdQKshi4BmM6FUwipjVOcBFGxqtQtirtAG4iZvHlqST9CpZKqlRjA==", - "dev": true, - "dependencies": { - "source-map": "^0.5.6" - } - }, - "node_modules/merge-source-map/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -27499,9 +27205,9 @@ } }, "node_modules/micromark": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.1.0.tgz", - "integrity": "sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", "dev": true, "funding": [ { @@ -27534,9 +27240,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz", - "integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", "dev": true, "funding": [ { @@ -27588,9 +27294,9 @@ } }, "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.4.tgz", - "integrity": "sha512-WCssN+M9rUyfHN5zPBn3/f0mIA7tqArHL/EKbv3CZK+LT2rG77FEikIQEqBkv46fOqXQK4NEW/Pc7Z27gshpeg==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", "dev": true, "dependencies": { "micromark-util-character": "^1.0.0", @@ -27604,9 +27310,9 @@ } }, "node_modules/micromark-extension-gfm-footnote": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.0.tgz", - "integrity": "sha512-RWYce7j8+c0n7Djzv5NzGEGitNNYO3uj+h/XYMdS/JinH1Go+/Qkomg/rfxExFzYTiydaV6GLeffGO5qcJbMPA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", "dev": true, "dependencies": { "micromark-core-commonmark": "^1.0.0", @@ -27624,9 +27330,9 @@ } }, "node_modules/micromark-extension-gfm-strikethrough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.5.tgz", - "integrity": "sha512-X0oI5eYYQVARhiNfbETy7BfLSmSilzN1eOuoRnrf9oUNsPRrWOAe9UqSizgw1vNxQBfOwL+n2610S3bYjVNi7w==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", "dev": true, "dependencies": { "micromark-util-chunked": "^1.0.0", @@ -27642,9 +27348,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.6.tgz", - "integrity": "sha512-92pq7Q+T+4kXH4M6kL+pc8WU23Z9iuhcqmtYFWdFWjm73ZscFpH2xE28+XFpGWlvgq3LUwcN0XC0PGCicYFpgA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", "dev": true, "dependencies": { "micromark-factory-space": "^1.0.0", @@ -27672,9 +27378,9 @@ } }, "node_modules/micromark-extension-gfm-task-list-item": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.4.tgz", - "integrity": "sha512-9XlIUUVnYXHsFF2HZ9jby4h3npfX10S1coXTnV035QGPgrtNYQq3J6IfIvcCIUAJrrqBVi5BqA/LmaOMJqPwMQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", "dev": true, "dependencies": { "micromark-factory-space": "^1.0.0", @@ -27689,9 +27395,9 @@ } }, "node_modules/micromark-factory-destination": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz", - "integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", "dev": true, "funding": [ { @@ -27710,9 +27416,9 @@ } }, "node_modules/micromark-factory-label": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz", - "integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", "dev": true, "funding": [ { @@ -27732,9 +27438,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz", - "integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", "dev": true, "funding": [ { @@ -27752,9 +27458,9 @@ } }, "node_modules/micromark-factory-title": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz", - "integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", "dev": true, "funding": [ { @@ -27770,14 +27476,13 @@ "micromark-factory-space": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" + "micromark-util-types": "^1.0.0" } }, "node_modules/micromark-factory-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz", - "integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", "dev": true, "funding": [ { @@ -27797,9 +27502,9 @@ } }, "node_modules/micromark-util-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz", - "integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", "dev": true, "funding": [ { @@ -27817,9 +27522,9 @@ } }, "node_modules/micromark-util-chunked": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz", - "integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", "dev": true, "funding": [ { @@ -27836,9 +27541,9 @@ } }, "node_modules/micromark-util-classify-character": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz", - "integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", "dev": true, "funding": [ { @@ -27857,9 +27562,9 @@ } }, "node_modules/micromark-util-combine-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz", - "integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", "dev": true, "funding": [ { @@ -27877,9 +27582,9 @@ } }, "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz", - "integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", "dev": true, "funding": [ { @@ -27896,9 +27601,9 @@ } }, "node_modules/micromark-util-decode-string": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz", - "integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", "dev": true, "funding": [ { @@ -27918,9 +27623,9 @@ } }, "node_modules/micromark-util-encode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz", - "integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", "dev": true, "funding": [ { @@ -27934,9 +27639,9 @@ ] }, "node_modules/micromark-util-html-tag-name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz", - "integrity": "sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", "dev": true, "funding": [ { @@ -27950,9 +27655,9 @@ ] }, "node_modules/micromark-util-normalize-identifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz", - "integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", "dev": true, "funding": [ { @@ -27969,9 +27674,9 @@ } }, "node_modules/micromark-util-resolve-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz", - "integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", "dev": true, "funding": [ { @@ -27988,9 +27693,9 @@ } }, "node_modules/micromark-util-sanitize-uri": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz", - "integrity": "sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", "dev": true, "funding": [ { @@ -28009,9 +27714,9 @@ } }, "node_modules/micromark-util-subtokenize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz", - "integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", "dev": true, "funding": [ { @@ -28031,9 +27736,9 @@ } }, "node_modules/micromark-util-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz", - "integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", "dev": true, "funding": [ { @@ -28047,9 +27752,9 @@ ] }, "node_modules/micromark-util-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz", - "integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", "dev": true, "funding": [ { @@ -28133,12 +27838,13 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", - "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", "dev": true, "dependencies": { - "schema-utils": "^4.0.0" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" }, "engines": { "node": ">= 12.13.0" @@ -28158,14 +27864,18 @@ "dev": true }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -28177,11 +27887,12 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minipass-collect": { @@ -28426,9 +28137,9 @@ "dev": true }, "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "dev": true, "engines": { "node": "*" @@ -28570,30 +28281,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/multimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/multimatch/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/multistream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", @@ -28618,20 +28305,6 @@ "readable-stream": "^3.6.0" } }, - "node_modules/multistream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/mute-stdout": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", @@ -28658,9 +28331,9 @@ } }, "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", "dev": true, "optional": true }, @@ -28726,13 +28399,12 @@ "dev": true }, "node_modules/needle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", - "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "optional": true, "dependencies": { - "debug": "^3.2.6", "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, @@ -28743,16 +28415,6 @@ "node": ">= 4.4.x" } }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "optional": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -28831,9 +28493,9 @@ } }, "node_modules/node-abi": { - "version": "3.51.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", - "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "version": "3.56.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz", + "integrity": "sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -28849,9 +28511,12 @@ "dev": true }, "node_modules/node-addon-api": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", - "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "engines": { + "node": "^16 || ^18 || >= 20" + } }, "node_modules/node-api-version": { "version": "0.2.0", @@ -28874,6 +28539,28 @@ "node": ">= 0.10.5" } }, + "node_modules/node-dir/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-dir/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", @@ -28894,9 +28581,9 @@ } }, "node_modules/node-fetch-native": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.2.0.tgz", - "integrity": "sha512-5IAMBTl9p6PaAjYCnMv5FmqIF6GcZnawAVnzaCG0rX2aYZJ4CxEkZNtVPuTRug7fL7wyM5BQYTlAzcyMPi6oTQ==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", "dev": true }, "node_modules/node-fetch/node_modules/tr46": { @@ -28927,12 +28614,13 @@ } }, "node_modules/node-gyp": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz", - "integrity": "sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", "dev": true, "dependencies": { "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", @@ -28951,9 +28639,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -28973,6 +28661,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/node-gyp/node_modules/gauge": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", @@ -29012,6 +28710,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/node-gyp/node_modules/nopt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", @@ -29042,20 +28752,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/node-gyp/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/node-gyp/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -29307,6 +29003,71 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm-registry-fetch/node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/cacache": { + "version": "17.1.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", + "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/cacache/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-registry-fetch/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/fs-minipass/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/npm-registry-fetch/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -29355,6 +29116,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/npm-registry-fetch/node_modules/minipass-fetch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", @@ -29381,6 +29151,51 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/npm-registry-fetch/node_modules/ssri": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", + "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/ssri/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-registry-fetch/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -29430,6 +29245,159 @@ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, + "node_modules/nypm": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.8.tgz", + "integrity": "sha512-IGWlC6So2xv6V4cIDmoV0SwwWx7zLG086gyqkyumteH2fIgCAM4nDVFB2iDRszDvmdSVW9xb1N+2KjQ6C7d4og==", + "dev": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "ufo": "^1.4.0" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/nypm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/nypm/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nypm/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -29464,57 +29432,23 @@ "node": ">=0.10.0" } }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-copy/node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/object-copy/node_modules/kind-of": { @@ -29547,13 +29481,13 @@ } }, "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -29592,13 +29526,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -29625,14 +29559,15 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -29642,15 +29577,17 @@ } }, "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/object.map": { @@ -29692,14 +29629,14 @@ } }, "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -29714,6 +29651,12 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true + }, "node_modules/oidc-client-ts": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-2.4.0.tgz", @@ -29854,6 +29797,36 @@ "readable-stream": "^2.0.1" } }, + "node_modules/ordered-read-streams/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/ordered-read-streams/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ordered-read-streams/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -29954,22 +29927,26 @@ } }, "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", "dev": true, "dependencies": { - "@types/retry": "0.12.0", + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-retry/node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, "node_modules/p-retry/node_modules/retry": { @@ -30022,6 +29999,134 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/pacote/node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/cacache": { + "version": "17.1.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", + "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/cacache/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pacote/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/fs-minipass/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pacote/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/pacote/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pacote/node_modules/ssri": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", + "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/ssri/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pacote/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/pako": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", @@ -30211,6 +30316,15 @@ "npm": ">5" } }, + "node_modules/patch-package/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/patch-package/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -30244,6 +30358,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/patch-package/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/patch-package/node_modules/open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", @@ -30278,14 +30403,6 @@ "node": ">=6" } }, - "node_modules/patch-package/node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "engines": { - "node": ">= 14" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -30367,23 +30484,14 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", - "integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, "engines": { "node": "14 || >=16.14" } }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz", - "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/path-to-regexp": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", @@ -30399,9 +30507,9 @@ } }, "node_modules/pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true }, "node_modules/pause-stream": { @@ -30428,9 +30536,9 @@ } }, "node_modules/pdfmake": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.8.tgz", - "integrity": "sha512-lI+amfIaUL8CrPhndxFdhIgMj9JB49Sj4DARltKC1gLm/5NsPohZqfB+D+II8HymtPB6eugUFD5oBxmzO57qHA==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.10.tgz", + "integrity": "sha512-doipFnmE1UHSk+Z3wfQuVweVQqx2pE/Ns2G5gCqZmWwqjDj+mZHnZYH/ryXWoIfD+iVdZUAutgI/VHkTCN+Xrw==", "dev": true, "dependencies": { "@foliojs-fork/linebreak": "^1.1.1", @@ -30453,6 +30561,36 @@ "through2": "^2.0.3" } }, + "node_modules/peek-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/peek-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/peek-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/peek-stream/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -30527,9 +30665,9 @@ } }, "node_modules/pirates": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", - "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, "engines": { "node": ">= 6" @@ -30652,6 +30790,23 @@ "node": ">= 6" } }, + "node_modules/pkg-fetch/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/pkg-fetch/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -30820,16 +30975,17 @@ } }, "node_modules/plist": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", - "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "dependencies": { + "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" }, "engines": { - "node": ">=6" + "node": ">=10.4.0" } }, "node_modules/plugin-error": { @@ -30863,9 +31019,9 @@ "dev": true }, "node_modules/polished": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", - "integrity": "sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", "dev": true, "dependencies": { "@babel/runtime": "^7.17.8" @@ -30893,10 +31049,19 @@ "node": ">=0.10.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", - "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -30915,7 +31080,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -30958,21 +31123,27 @@ } }, "node_modules/postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, "engines": { "node": ">= 14" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" @@ -30986,35 +31157,35 @@ } } }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", - "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", "dev": true, "dependencies": { - "cosmiconfig": "^8.3.5", + "cosmiconfig": "^9.0.0", "jiti": "^1.20.0", "semver": "^7.5.4" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { + "@rspack/core": "0.x || 1.x", "postcss": "^7.0.0 || ^8.0.1", "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/postcss-loader/node_modules/argparse": { @@ -31024,15 +31195,15 @@ "dev": true }, "node_modules/postcss-loader/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "dependencies": { + "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" @@ -31074,9 +31245,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -31091,9 +31262,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -31140,9 +31311,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -31209,9 +31380,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.12.tgz", - "integrity": "sha512-o74kiDBVE73oHW+pdkFSluHBL3cYEvru5YgEqNkBMFF7Cjv+w1vI565lTlfoJT4VLWDe0FMtZ7FkE/7a4pMXSQ==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.13.tgz", + "integrity": "sha512-2tPWHCFNC+WRjAC4SIWQNSOdcL1NNkydXim8w7TDqlZi+/ulZYz2OouAI6qMtkggnPt7lGamboj6LcTMwcCvoQ==", "dev": true, "engines": { "node": ">=14.21.3" @@ -31221,6 +31392,7 @@ "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", @@ -31246,6 +31418,9 @@ "@trivago/prettier-plugin-sort-imports": { "optional": true }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, "prettier-plugin-astro": { "optional": true }, @@ -31275,9 +31450,6 @@ }, "prettier-plugin-svelte": { "optional": true - }, - "prettier-plugin-twig-melody": { - "optional": true } } }, @@ -31304,17 +31476,17 @@ } }, "node_modules/pretty-format": { - "version": "29.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", - "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.4.3", + "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "react-is": "^17.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -31550,6 +31722,16 @@ "node": ">= 6.0.0" } }, + "node_modules/puppeteer-core/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/puppeteer-core/node_modules/extract-zip": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", @@ -31607,6 +31789,18 @@ "node": ">= 6.0.0" } }, + "node_modules/puppeteer-core/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/puppeteer-core/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -31647,9 +31841,9 @@ } }, "node_modules/pure-rand": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", - "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", "dev": true, "funding": [ { @@ -31677,11 +31871,11 @@ "integrity": "sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g==" }, "node_modules/qs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", - "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", + "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -31733,39 +31927,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/quote-stream": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", - "integrity": "sha512-kKr2uQ2AokadPjvTyKJQad9xELbZwYzWlNfI3Uz2j/ib5u6H9lDP7fUUR//rMycd0gv4Z5P1qXMfXR8YpIxrjQ==", - "dev": true, - "dependencies": { - "buffer-equal": "0.0.1", - "minimist": "^1.1.3", - "through2": "^2.0.0" - }, - "bin": { - "quote-stream": "bin/cmd.js" - } - }, - "node_modules/quote-stream/node_modules/buffer-equal": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", - "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/quote-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, "node_modules/ramda": { "version": "0.29.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", @@ -31795,9 +31956,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -31816,21 +31977,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/raw-body/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -31908,9 +32054,9 @@ } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, "node_modules/react-remove-scroll": { @@ -31939,9 +32085,9 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", "dev": true, "dependencies": { "react-style-singleton": "^2.2.1", @@ -32100,37 +32246,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", @@ -32140,21 +32255,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -32267,9 +32367,9 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -32285,24 +32385,18 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/readdir-glob": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", @@ -32313,16 +32407,6 @@ "minimatch": "^5.1.0" } }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/readdir-glob/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -32349,33 +32433,21 @@ } }, "node_modules/recast": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.4.tgz", - "integrity": "sha512-qtEDqIZGVcSZCHniWwZWbRy79Dc6Wp3kT/UmDA2RJKBPg7+7k51aQBZirHmUGn5uvHf2rg8DkjizrN26k61ATw==", + "version": "0.23.6", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.6.tgz", + "integrity": "sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==", "dev": true, "dependencies": { - "assert": "^2.0.0", "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" }, "engines": { "node": ">= 4" } }, - "node_modules/recast/node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/recast/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -32411,9 +32483,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "dev": true }, "node_modules/regedit": { @@ -32469,9 +32541,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -32509,20 +32581,21 @@ } }, "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -32669,6 +32742,36 @@ "node": ">= 0.10" } }, + "node_modules/remove-bom-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/remove-bom-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/remove-bom-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/remove-bom-stream/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -32804,12 +32907,12 @@ } }, "node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/replace-homedir": { @@ -32846,6 +32949,36 @@ "node": ">=0.8.0" } }, + "node_modules/replacestream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/replacestream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/replacestream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -32953,6 +33086,15 @@ "node": ">= 0.10" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", @@ -33069,9 +33211,9 @@ } }, "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "dev": true }, "node_modules/rimraf": { @@ -33092,61 +33234,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -33166,9 +33253,9 @@ } }, "node_modules/roarr/node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true, "optional": true }, @@ -33193,6 +33280,18 @@ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -33250,6 +33349,16 @@ "rxjs-report-usage": "bin/rxjs-report-usage" } }, + "node_modules/rxjs-report-usage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/rxjs-report-usage/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -33270,6 +33379,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rxjs-report-usage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -33283,13 +33404,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -33315,15 +33436,18 @@ } }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -33343,9 +33467,9 @@ } }, "node_modules/sass": { - "version": "1.69.5", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", - "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "version": "1.74.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.74.1.tgz", + "integrity": "sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -33397,9 +33521,9 @@ } }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", "dev": true }, "node_modules/saxes": { @@ -33423,9 +33547,9 @@ } }, "node_modules/schema-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", - "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", @@ -33441,21 +33565,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/scope-analyzer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/scope-analyzer/-/scope-analyzer-2.1.2.tgz", - "integrity": "sha512-5cfCmsTYV/wPaRIItNxatw02ua/MThdIUNnUOCYp+3LSEJvnG804ANw2VLaavNILIfWXF1D1G2KNANkBBvInwQ==", - "dev": true, - "dependencies": { - "array-from": "^2.1.1", - "dash-ast": "^2.0.1", - "es6-map": "^0.1.5", - "es6-set": "^0.1.5", - "es6-symbol": "^3.1.1", - "estree-is-function": "^1.0.0", - "get-assigned-identifiers": "^1.1.0" - } - }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -33463,11 +33572,12 @@ "dev": true }, "node_modules/selfsigned": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", - "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -33524,33 +33634,32 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/send/-/send-1.0.0-beta.2.tgz", + "integrity": "sha512-k1yHu/FNK745PULKdsGpQ+bVSXYNwSk+bWnYzbxGZbt5obZc0JKDVANsCRuJD1X/EG15JtP9eZpwxkhUxIYEcg==", "dev": true, "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", + "debug": "3.1.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "mime": "1.6.0", + "mime-types": "~2.1.34", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.10" } }, "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "dependencies": { "ms": "2.0.0" @@ -33562,34 +33671,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/send/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -33626,9 +33707,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -33727,6 +33808,63 @@ "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-static/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -33738,28 +33876,31 @@ "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -33844,12 +33985,6 @@ "node": ">=0.10.0" } }, - "node_modules/shallow-copy": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", - "integrity": "sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==", - "dev": true - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -33879,13 +34014,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -33915,6 +34054,53 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/sigstore/node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/cacache": { + "version": "17.1.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", + "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/sigstore/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -33963,6 +34149,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/sigstore/node_modules/make-fetch-happen/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/sigstore/node_modules/minipass-fetch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", @@ -33980,13 +34175,40 @@ "encoding": "^0.1.13" } }, - "node_modules/sigstore/node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "node_modules/sigstore/node_modules/ssri": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", + "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/sigstore/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/simple-concat": { @@ -34194,72 +34416,17 @@ "node": ">=0.10.0" } }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, "node_modules/snapdragon/node_modules/is-extendable": { @@ -34297,17 +34464,26 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz", + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -34335,9 +34511,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -34447,9 +34623,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -34463,9 +34639,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, "node_modules/spdy": { @@ -34498,20 +34674,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -34543,26 +34705,35 @@ "dev": true }, "node_modules/ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", "dev": true, "dependencies": { - "minipass": "^7.0.3" + "minipass": "^3.1.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/ssri/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" } }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -34602,107 +34773,6 @@ "node": ">= 6" } }, - "node_modules/static-eval": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.0.tgz", - "integrity": "sha512-agtxZ/kWSsCkI5E4QifRwsaPs0P0JmZV6dkLz6ILYfFYQGn+5plctanRN+IC8dJRiFkyXHrwEE3W9Wmx67uDbw==", - "dev": true, - "dependencies": { - "escodegen": "^1.11.1" - } - }, - "node_modules/static-eval/node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-eval/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/static-eval/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-eval/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-eval/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -34728,205 +34798,17 @@ "node": ">=0.10.0" } }, - "node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-module": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/static-module/-/static-module-3.0.4.tgz", - "integrity": "sha512-gb0v0rrgpBkifXCa3yZXxqVmXDVE+ETXj6YlC/jt5VzOnGXR2C15+++eXuMDUYsePnbhf+lwW0pE1UXyOLtGCw==", - "dev": true, - "dependencies": { - "acorn-node": "^1.3.0", - "concat-stream": "~1.6.0", - "convert-source-map": "^1.5.1", - "duplexer2": "~0.1.4", - "escodegen": "^1.11.1", - "has": "^1.0.1", - "magic-string": "0.25.1", - "merge-source-map": "1.0.4", - "object-inspect": "^1.6.0", - "readable-stream": "~2.3.3", - "scope-analyzer": "^2.0.1", - "shallow-copy": "~0.0.1", - "static-eval": "^2.0.5", - "through2": "~2.0.3" - } - }, - "node_modules/static-module/node_modules/escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/static-module/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/static-module/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-module/node_modules/magic-string": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz", - "integrity": "sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==", - "dev": true, - "dependencies": { - "sourcemap-codec": "^1.4.1" - } - }, - "node_modules/static-module/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-module/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/static-module/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-module/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/static-module/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" + "node": ">= 0.4" } }, "node_modules/statuses": { @@ -34958,9 +34840,9 @@ } }, "node_modules/store2": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.2.tgz", - "integrity": "sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==", + "version": "2.14.3", + "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.3.tgz", + "integrity": "sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==", "dev": true }, "node_modules/storybook": { @@ -35005,10 +34887,40 @@ "readable-stream": "^2.1.4" } }, + "node_modules/stream-meter/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/stream-meter/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-meter/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, "node_modules/stream-slicer": { @@ -35029,20 +34941,6 @@ "node": ">=8.12.0" } }, - "node_modules/streamfilter/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/streaming-json-stringify": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz", @@ -35052,6 +34950,33 @@ "readable-stream": "2" } }, + "node_modules/streaming-json-stringify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/streaming-json-stringify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/streaming-json-stringify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -35061,23 +34986,45 @@ } }, "node_modules/streamx": { - "version": "2.15.5", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz", - "integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", "dev": true, "dependencies": { "fast-fifo": "^1.1.0", "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -35130,14 +35077,15 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -35147,28 +35095,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -35258,14 +35209,14 @@ } }, "node_modules/sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "7.1.6", + "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", @@ -35276,7 +35227,7 @@ "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/sucrase/node_modules/commander": { @@ -35288,26 +35239,6 @@ "node": ">= 6" } }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -35360,10 +35291,13 @@ "dev": true }, "node_modules/swc-loader": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.3.tgz", - "integrity": "sha512-D1p6XXURfSPleZZA/Lipb3A8pZ17fP4NObZvFCDjK/OKljroqDpPmsBdTraWhVBqUNpcWBQY1imWdoPScRlQ7A==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", + "integrity": "sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==", "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + }, "peerDependencies": { "@swc/core": "^1.2.147", "webpack": ">=2" @@ -35449,6 +35383,15 @@ "node": ">=10.13.0" } }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -35459,9 +35402,9 @@ } }, "node_modules/tar": { - "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -35508,38 +35451,10 @@ "node": ">=6" } }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "engines": { "node": ">=8" } @@ -35598,6 +35513,30 @@ "fs-extra": "^10.0.0" } }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/temp/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -35618,6 +35557,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/temp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/temp/node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -35674,29 +35625,15 @@ } }, "node_modules/ternary-stream/node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "dev": true, "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "node_modules/ternary-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "stream-shift": "^1.0.2" } }, "node_modules/terser": { @@ -35718,16 +35655,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -35803,9 +35740,9 @@ "dev": true }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", @@ -35836,13 +35773,13 @@ } }, "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.17.6", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.6.tgz", - "integrity": "sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ==", + "version": "5.29.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", + "integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==", "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -35873,6 +35810,16 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -35893,6 +35840,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -35957,6 +35916,36 @@ "xtend": "~4.0.0" } }, + "node_modules/through2-filter/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/through2-filter/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2-filter/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/through2-filter/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -35989,9 +35978,9 @@ "dev": true }, "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true }, "node_modules/tiny-typed-emitter": { @@ -36001,20 +35990,20 @@ "dev": true }, "node_modules/tldts": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.13.tgz", - "integrity": "sha512-+GxHFKVHvUTg2ieNPTx3b/NpZbgJSTZEDdI4cJzTjVYDuxijeHi1tt7CHHsMjLqyc+T50VVgWs3LIb2LrXOzxw==", + "version": "6.1.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.16.tgz", + "integrity": "sha512-X6VrQzW4RymhI1kBRvrWzYlRLXTftZpi7/s/9ZlDILA04yM2lNX7mBvkzDib9L4uSymHt8mBbeaielZMdsAkfQ==", "dependencies": { - "tldts-core": "^6.1.13" + "tldts-core": "^6.1.16" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.13.tgz", - "integrity": "sha512-M1XP4D13YtXARKroULnLsKKuI1NCRAbJmUGGoXqWinajIDOhTeJf/trYUyBoLVx1/Nx1KBKxCrlW57ZW9cMHAA==" + "version": "6.1.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.16.tgz", + "integrity": "sha512-rxnuCux+zn3hMF57nBzr1m1qGZH7Od2ErbDZjVm04fk76cEynTg3zqvHjx5BsBl8lvRTjpzIhsEGMHDH/Hr2Vw==" }, "node_modules/tmp": { "version": "0.0.33", @@ -36036,51 +36025,13 @@ "tmp": "^0.2.0" } }, - "node_modules/tmp-promise/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tmp-promise/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/tmp-promise/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/tmpl": { @@ -36090,9 +36041,9 @@ "dev": true }, "node_modules/to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-3.0.0.tgz", + "integrity": "sha512-loO/XEWTRqpfcpI7+Jr2RR2Umaaozx1t6OSVWtMi0oy5F/Fxg3IC+D/TToDnxyAGs7uZBGT/6XmyDUxgsObJXA==", "dev": true, "dependencies": { "is-absolute": "^1.0.0", @@ -36178,6 +36129,36 @@ "node": ">= 0.10" } }, + "node_modules/to-through/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/to-through/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/to-through/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/to-through/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -36236,10 +36217,13 @@ } }, "node_modules/traverse": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", - "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", + "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -36254,9 +36238,9 @@ } }, "node_modules/trough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", "dev": true, "funding": { "type": "github", @@ -36273,12 +36257,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -36517,6 +36501,53 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/tuf-js/node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/cacache": { + "version": "17.1.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", + "integrity": "sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^7.0.3", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/tuf-js/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -36565,6 +36596,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/tuf-js/node_modules/make-fetch-happen/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/tuf-js/node_modules/minipass-fetch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", @@ -36582,13 +36622,40 @@ "encoding": "^0.1.13" } }, - "node_modules/tuf-js/node_modules/minipass-fetch/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "node_modules/tuf-js/node_modules/ssri": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", + "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tuf-js/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/tunnel-agent": { @@ -36604,9 +36671,9 @@ } }, "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", "dev": true }, "node_modules/type-check": { @@ -36655,29 +36722,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -36687,16 +36755,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -36706,14 +36775,20 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -36743,6 +36818,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "dev": true + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -36882,6 +36963,18 @@ "tiny-inflate": "^1.0.0" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -36926,27 +37019,27 @@ } }, "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", "dev": true, "dependencies": { - "unique-slug": "^4.0.0" + "unique-slug": "^3.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", "dev": true, "dependencies": { "imurmurhash": "^0.1.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/unique-stream": { @@ -37051,9 +37144,9 @@ } }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { "node": ">= 10.0.0" } @@ -37073,15 +37166,42 @@ } }, "node_modules/unplugin": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.7.1.tgz", - "integrity": "sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.10.0.tgz", + "integrity": "sha512-CuZtvvO8ua2Wl+9q2jEaqH6m3DoQ38N7pvBYQbbaeNlWGvK2l6GHiKi29aIHDPoSxdUzQ7Unevf1/ugil5X6Pg==", "dev": true, "dependencies": { "acorn": "^8.11.3", - "chokidar": "^3.5.3", + "chokidar": "^3.6.0", "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.6.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/unplugin/node_modules/webpack-virtual-modules": { @@ -37243,9 +37363,9 @@ } }, "node_modules/use-callback-ref": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", - "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", "dev": true, "dependencies": { "tslib": "^2.0.0" @@ -37350,10 +37470,14 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -37376,15 +37500,6 @@ "node": ">=8" } }, - "node_modules/uvu/node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/uvu/node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -37395,20 +37510,27 @@ } }, "node_modules/v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dev": true, "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "peer": true + }, "node_modules/v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", @@ -37475,13 +37597,6 @@ "node": ">=0.6.0" } }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "optional": true - }, "node_modules/vfile": { "version": "5.3.7", "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", @@ -37513,20 +37628,19 @@ } }, "node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", + "clone": "^2.1.2", "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/vinyl-fs": { @@ -37557,6 +37671,54 @@ "node": ">= 0.10" } }, + "node_modules/vinyl-fs/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/vinyl-fs/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/vinyl-fs/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/vinyl-fs/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-fs/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/vinyl-fs/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -37567,6 +37729,23 @@ "xtend": "~4.0.1" } }, + "node_modules/vinyl-fs/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vinyl-sourcemap": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", @@ -37585,6 +37764,15 @@ "node": ">= 0.10" } }, + "node_modules/vinyl-sourcemap/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/vinyl-sourcemap/node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", @@ -37597,6 +37785,32 @@ "node": ">=0.10.0" } }, + "node_modules/vinyl-sourcemap/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vinyl/node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -37686,15 +37900,14 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/wait-on": { @@ -37726,9 +37939,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -37914,54 +38127,54 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", - "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", "dev": true, "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "default-gateway": "^6.0.3", "express": "^4.17.3", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", + "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -37972,33 +38185,40 @@ } } }, - "node_modules/webpack-dev-server/node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "dependencies": { - "@types/node": "*" - } - }, - "node_modules/webpack-dev-server/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": "*" + "node": ">= 8.10.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { @@ -38010,69 +38230,88 @@ "node": ">= 10" } }, - "node_modules/webpack-dev-server/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/webpack-dev-server/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "is-inside-container": "^1.0.0" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">=16" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/memfs": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.8.1.tgz", + "integrity": "sha512-7q/AdPzf2WpwPlPL4v1kE2KsJsHl7EF4+hAeVzlyanr2+YnR21NVn9mDqo+7DEaKDRsQy8nvxPlKH4WqMtiO0w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz", + "integrity": "sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA==", "dev": true, "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { + "webpack": { "optional": true } } }, "node_modules/webpack-hot-middleware": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.25.4.tgz", - "integrity": "sha512-IRmTspuHM06aZh98OhBJtqLpeWFM8FXJS5UYpKYxCJzyFoyWj1w6VGFfomZU7OPA55dMLrQK0pRT1eQ3PACr4w==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz", + "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==", "dev": true, "dependencies": { "ansi-html-community": "0.0.8", @@ -38233,24 +38472,22 @@ } }, "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dependencies": { "iconv-lite": "0.6.3" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true, + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { @@ -38296,15 +38533,18 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -38317,16 +38557,16 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -38411,15 +38651,6 @@ "node": ">=8.12.0" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -38427,20 +38658,16 @@ "dev": true }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/wrap-ansi-cjs": { @@ -38480,15 +38707,15 @@ } }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -38506,12 +38733,11 @@ "dev": true }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlbuilder": { @@ -38560,18 +38786,17 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -38677,6 +38902,17 @@ "node": ">= 10" } }, + "node_modules/zip-stream/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/zip-stream/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -38698,19 +38934,17 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/zip-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/zip-stream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "peer": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 6" + "node": "*" } }, "node_modules/zone.js": { diff --git a/package.json b/package.json index b64c0a4024..057e737903 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "18.19.19", + "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.0", @@ -78,17 +78,17 @@ "@types/react": "16.14.57", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "6.21.0", - "@typescript-eslint/parser": "6.21.0", + "@typescript-eslint/eslint-plugin": "7.4.0", + "@typescript-eslint/parser": "7.4.0", "@webcomponents/custom-elements": "1.6.0", "autoprefixer": "10.4.18", "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", - "copy-webpack-plugin": "11.0.0", + "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", - "css-loader": "6.8.1", - "electron": "28.2.8", + "css-loader": "6.10.0", + "electron": "28.3.1", "electron-builder": "24.13.3", "electron-log": "5.0.1", "electron-reload": "2.0.0-alpha.1", @@ -108,28 +108,28 @@ "gulp-json-editor": "2.6.0", "gulp-replace": "1.1.4", "gulp-zip": "6.0.0", - "html-loader": "4.2.0", + "html-loader": "5.0.0", "html-webpack-injector": "1.1.4", - "html-webpack-plugin": "5.5.4", + "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.5", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", - "mini-css-extract-plugin": "2.7.6", + "mini-css-extract-plugin": "2.8.1", "node-ipc": "9.2.1", "pkg": "5.8.1", - "postcss": "8.4.35", - "postcss-loader": "7.3.4", + "postcss": "8.4.38", + "postcss-loader": "8.1.1", "prettier": "3.2.2", - "prettier-plugin-tailwindcss": "0.5.12", + "prettier-plugin-tailwindcss": "0.5.13", "process": "0.11.10", "react": "18.2.0", "react-dom": "18.2.0", "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", - "sass": "1.69.5", + "sass": "1.74.1", "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4", @@ -144,7 +144,7 @@ "wait-on": "7.2.0", "webpack": "5.89.0", "webpack-cli": "5.1.4", - "webpack-dev-server": "4.15.1", + "webpack-dev-server": "5.0.4", "webpack-node-externals": "3.0.0" }, "dependencies": { @@ -171,7 +171,7 @@ "bufferutil": "4.0.8", "chalk": "4.1.2", "commander": "11.1.0", - "core-js": "3.34.0", + "core-js": "3.36.1", "duo_web_sdk": "github:duosecurity/duo_web_sdk", "form-data": "4.0.0", "https-proxy-agent": "7.0.2", @@ -200,7 +200,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.13", + "tldts": "6.1.16", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -220,7 +220,7 @@ "*.ts": "eslint --cache --cache-strategy content --fix" }, "engines": { - "node": "~18", + "node": "^18.18.0", "npm": "~9" } }